Fix 8 design majors: optional ILogger in libraries, IndexFactoryService singleton threading, SharedMediaTypeRegistry, required TrackDto fields, GetExtensionType dedup, PagedResult zero-guard, TrackService null-return on failure
This commit is contained in:
@@ -10,4 +10,8 @@
|
||||
<ProjectReference Include="..\DeepDrftModels\DeepDrftModels.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -3,12 +3,20 @@ using DeepDrftContent.Services.FileDatabase.Services;
|
||||
|
||||
namespace DeepDrftContent.Services.FileDatabase.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Shared media type registry instance — one allocation for all factory classes in this file.
|
||||
/// </summary>
|
||||
file static class SharedMediaTypeRegistry
|
||||
{
|
||||
internal static readonly IMediaTypeRegistry Instance = new SimpleMediaTypeRegistry();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Type mappings for media vault types - simple dictionary-based approach
|
||||
/// </summary>
|
||||
public static class MediaVaultTypeMap
|
||||
{
|
||||
private static readonly IMediaTypeRegistry _registry = new SimpleMediaTypeRegistry();
|
||||
private static readonly IMediaTypeRegistry _registry = SharedMediaTypeRegistry.Instance;
|
||||
|
||||
public static Type GetBinaryType(MediaVaultType vaultType) => _registry.GetBinaryType(vaultType);
|
||||
|
||||
@@ -55,7 +63,7 @@ public static class MetaDataFactory
|
||||
return new AudioMetaData(entryKey, extension, duration, bitrate);
|
||||
}
|
||||
|
||||
private static readonly IMediaTypeRegistry _metaDataRegistry = new SimpleMediaTypeRegistry();
|
||||
private static readonly IMediaTypeRegistry _metaDataRegistry = SharedMediaTypeRegistry.Instance;
|
||||
|
||||
public static MetaData CreateFromMedia(MediaVaultType type, string entryKey, string extension, object media)
|
||||
{
|
||||
@@ -75,7 +83,7 @@ public static class MetaDataFactory
|
||||
/// </summary>
|
||||
public static class MediaParamsFactory
|
||||
{
|
||||
private static readonly IMediaTypeRegistry _registry = new SimpleMediaTypeRegistry();
|
||||
private static readonly IMediaTypeRegistry _registry = SharedMediaTypeRegistry.Instance;
|
||||
|
||||
public static object Create(MediaVaultType type, FileBinary fileBinary, MetaData metaData)
|
||||
{
|
||||
@@ -94,7 +102,7 @@ public static class MediaParamsFactory
|
||||
/// </summary>
|
||||
public static class FileBinaryFactory
|
||||
{
|
||||
private static readonly IMediaTypeRegistry _registry = new SimpleMediaTypeRegistry();
|
||||
private static readonly IMediaTypeRegistry _registry = SharedMediaTypeRegistry.Instance;
|
||||
|
||||
public static object Create(MediaVaultType vaultType, object parameters)
|
||||
{
|
||||
@@ -124,7 +132,7 @@ public static class FileBinaryFactory
|
||||
/// </summary>
|
||||
public static class FileBinaryDtoFactory
|
||||
{
|
||||
private static readonly IMediaTypeRegistry _registry = new SimpleMediaTypeRegistry();
|
||||
private static readonly IMediaTypeRegistry _registry = SharedMediaTypeRegistry.Instance;
|
||||
|
||||
public static object From(MediaVaultType type, object mediaBinary)
|
||||
{
|
||||
|
||||
@@ -68,7 +68,7 @@ public class MediaBinary : FileBinary
|
||||
return new MediaBinary(new MediaBinaryParams(buffer, dto.Size, extension));
|
||||
}
|
||||
|
||||
private static string GetExtensionType(string mime)
|
||||
protected static string GetExtensionType(string mime)
|
||||
{
|
||||
return MimeTypeExtensions.GetExtension(mime);
|
||||
}
|
||||
@@ -116,11 +116,6 @@ public class ImageBinary : MediaBinary
|
||||
var extension = GetExtensionType(dto.Mime);
|
||||
return new ImageBinary(new ImageBinaryParams(buffer, dto.Size, extension, dto.AspectRatio));
|
||||
}
|
||||
|
||||
private static string GetExtensionType(string mime)
|
||||
{
|
||||
return MimeTypeExtensions.GetExtension(mime);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -171,11 +166,6 @@ public class AudioBinary : MediaBinary
|
||||
var extension = GetExtensionType(dto.Mime);
|
||||
return new AudioBinary(new AudioBinaryParams(buffer, dto.Size, extension, dto.Duration, dto.Bitrate));
|
||||
}
|
||||
|
||||
private static string GetExtensionType(string mime)
|
||||
{
|
||||
return MimeTypeExtensions.GetExtension(mime);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -11,6 +11,7 @@ public class FileDatabase : DirectoryIndexDirectory, IDisposable
|
||||
{
|
||||
private readonly StructuralMap<string, MediaVault> _vaults;
|
||||
private readonly IndexWatcher _indexWatcher;
|
||||
private readonly IndexFactoryService _indexFactory;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
@@ -23,7 +24,7 @@ public class FileDatabase : DirectoryIndexDirectory, IDisposable
|
||||
|
||||
if (rootIndex != null)
|
||||
{
|
||||
var db = new FileDatabase(rootPath, rootIndex);
|
||||
var db = new FileDatabase(rootPath, rootIndex, factoryService);
|
||||
await db.InitVaultsAsync();
|
||||
return db;
|
||||
}
|
||||
@@ -31,10 +32,11 @@ public class FileDatabase : DirectoryIndexDirectory, IDisposable
|
||||
return null;
|
||||
}
|
||||
|
||||
private FileDatabase(string rootPath, IDirectoryIndex index) : base(rootPath, index)
|
||||
private FileDatabase(string rootPath, IDirectoryIndex index, IndexFactoryService indexFactory) : base(rootPath, index)
|
||||
{
|
||||
_vaults = new StructuralMap<string, MediaVault>();
|
||||
_indexWatcher = new IndexWatcher();
|
||||
_indexFactory = indexFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -58,7 +60,7 @@ public class FileDatabase : DirectoryIndexDirectory, IDisposable
|
||||
private async Task InitVaultAsync(string vaultId, MediaVaultType vaultType)
|
||||
{
|
||||
var path = Path.Combine(RootPath, vaultId);
|
||||
var directoryVault = await MediaVaultFactory.From(path, vaultType);
|
||||
var directoryVault = await MediaVaultFactory.From(path, vaultType, _indexFactory);
|
||||
|
||||
if (directoryVault != null)
|
||||
{
|
||||
@@ -80,9 +82,8 @@ public class FileDatabase : DirectoryIndexDirectory, IDisposable
|
||||
{
|
||||
try
|
||||
{
|
||||
var factoryService = new IndexFactoryService();
|
||||
var vaultPath = Path.Combine(RootPath, vaultId);
|
||||
var index = await factoryService.LoadIndexAsync(IndexType.Vault, vaultPath);
|
||||
var index = await _indexFactory.LoadIndexAsync(IndexType.Vault, vaultPath);
|
||||
|
||||
if (index is VaultIndex vaultIndex)
|
||||
{
|
||||
@@ -115,25 +116,18 @@ public class FileDatabase : DirectoryIndexDirectory, IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new vault
|
||||
/// Creates a new vault. Propagates exceptions to the caller — vault creation failure is not
|
||||
/// silently swallowable because a partially-created vault would leave the index inconsistent.
|
||||
/// </summary>
|
||||
public async Task CreateVaultAsync(string vaultId, MediaVaultType vaultType)
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = Path.Combine(RootPath, vaultId);
|
||||
var directoryVault = await MediaVaultFactory.From(path, vaultType);
|
||||
var path = Path.Combine(RootPath, vaultId);
|
||||
var directoryVault = await MediaVaultFactory.From(path, vaultType, _indexFactory);
|
||||
|
||||
if (directoryVault != null)
|
||||
{
|
||||
_vaults.Set(vaultId, directoryVault);
|
||||
// Now using string-based index
|
||||
await AddToIndexAsync(vaultId);
|
||||
}
|
||||
}
|
||||
catch
|
||||
if (directoryVault != null)
|
||||
{
|
||||
throw;
|
||||
_vaults.Set(vaultId, directoryVault);
|
||||
await AddToIndexAsync(vaultId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using DeepDrftContent.Services.FileDatabase.Abstractions;
|
||||
using DeepDrftContent.Services.FileDatabase.Models;
|
||||
using DeepDrftContent.Services.FileDatabase.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace DeepDrftContent.Services.FileDatabase.Services;
|
||||
|
||||
@@ -96,12 +97,15 @@ public class VaultIndexDirectory : IndexDirectory
|
||||
{
|
||||
private IVaultIndex _vaultIndex;
|
||||
private readonly SemaphoreSlim _indexLock = new(1, 1);
|
||||
private readonly IndexFactoryService _factoryService = new();
|
||||
private readonly IndexFactoryService _factoryService;
|
||||
private readonly ILogger<VaultIndexDirectory>? _logger;
|
||||
|
||||
public VaultIndexDirectory(string rootPath, IVaultIndex index, IIndexDataFactory? indexDataFactory = null)
|
||||
: base(rootPath, IndexType.Vault, index, indexDataFactory)
|
||||
public VaultIndexDirectory(string rootPath, IVaultIndex index, IIndexDataFactory? indexDataFactory = null, ILogger<VaultIndexDirectory>? logger = null, IndexFactoryService? factoryService = null)
|
||||
: base(rootPath, IndexType.Vault, index, indexDataFactory ?? factoryService)
|
||||
{
|
||||
_vaultIndex = index;
|
||||
_logger = logger;
|
||||
_factoryService = factoryService ?? new IndexFactoryService();
|
||||
}
|
||||
|
||||
protected async Task AddToIndexAsync(string entryId, MetaData metaData)
|
||||
@@ -131,12 +135,18 @@ public class VaultIndexDirectory : IndexDirectory
|
||||
{
|
||||
_vaultIndex = vaultIndex;
|
||||
Index = vaultIndex;
|
||||
Console.WriteLine($"VaultIndexDirectory: Reloaded index for {RootPath}, {vaultIndex.GetEntriesSize()} entries");
|
||||
if (_logger != null)
|
||||
_logger.LogDebug("VaultIndexDirectory: Reloaded index for {RootPath}, {EntryCount} entries", RootPath, vaultIndex.GetEntriesSize());
|
||||
else
|
||||
Console.WriteLine($"VaultIndexDirectory: Reloaded index for {RootPath}, {vaultIndex.GetEntriesSize()} entries");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"VaultIndexDirectory: Failed to reload index for {RootPath}: {ex.Message}");
|
||||
if (_logger != null)
|
||||
_logger.LogWarning(ex, "VaultIndexDirectory: Failed to reload index for {RootPath}", RootPath);
|
||||
else
|
||||
Console.WriteLine($"VaultIndexDirectory: Failed to reload index for {RootPath}: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using DeepDrftContent.Services.FileDatabase.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace DeepDrftContent.Services.FileDatabase.Services;
|
||||
|
||||
@@ -11,8 +12,14 @@ public class IndexWatcher : IDisposable
|
||||
private readonly Dictionary<string, FileSystemWatcher> _watchers = new();
|
||||
private readonly Dictionary<string, Action> _reloadCallbacks = new();
|
||||
private readonly object _lock = new();
|
||||
private readonly ILogger<IndexWatcher>? _logger;
|
||||
private bool _disposed;
|
||||
|
||||
public IndexWatcher(ILogger<IndexWatcher>? logger = null)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers an index file to be watched for changes.
|
||||
/// </summary>
|
||||
@@ -46,11 +53,17 @@ public class IndexWatcher : IDisposable
|
||||
_watchers[indexPath] = watcher;
|
||||
_reloadCallbacks[indexPath] = onChanged;
|
||||
|
||||
Console.WriteLine($"IndexWatcher: Watching {indexPath}/index");
|
||||
if (_logger != null)
|
||||
_logger.LogDebug("IndexWatcher: Watching {IndexPath}/index", indexPath);
|
||||
else
|
||||
Console.WriteLine($"IndexWatcher: Watching {indexPath}/index");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"IndexWatcher: Failed to watch {indexPath}: {ex.Message}");
|
||||
if (_logger != null)
|
||||
_logger.LogWarning(ex, "IndexWatcher: Failed to watch {IndexPath}", indexPath);
|
||||
else
|
||||
Console.WriteLine($"IndexWatcher: Failed to watch {indexPath}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,7 +96,10 @@ public class IndexWatcher : IDisposable
|
||||
{
|
||||
if (_reloadCallbacks.TryGetValue(indexPath, out var callback))
|
||||
{
|
||||
Console.WriteLine($"IndexWatcher: Index changed at {indexPath}, triggering reload");
|
||||
if (_logger != null)
|
||||
_logger.LogDebug("IndexWatcher: Index changed at {IndexPath}, triggering reload", indexPath);
|
||||
else
|
||||
Console.WriteLine($"IndexWatcher: Index changed at {indexPath}, triggering reload");
|
||||
|
||||
// Invoke callback on a background thread to avoid blocking the watcher
|
||||
Task.Run(() =>
|
||||
@@ -94,7 +110,10 @@ public class IndexWatcher : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"IndexWatcher: Reload callback failed: {ex.Message}");
|
||||
if (_logger != null)
|
||||
_logger.LogWarning(ex, "IndexWatcher: Reload callback failed for {IndexPath}", indexPath);
|
||||
else
|
||||
Console.WriteLine($"IndexWatcher: Reload callback failed: {ex.Message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ namespace DeepDrftContent.Services.FileDatabase.Services;
|
||||
/// </summary>
|
||||
public abstract class MediaVault : VaultIndexDirectory
|
||||
{
|
||||
protected MediaVault(string rootPath, VaultIndex index) : base(rootPath, index) { }
|
||||
protected MediaVault(string rootPath, VaultIndex index, IndexFactoryService? factoryService = null)
|
||||
: base(rootPath, index, factoryService: factoryService) { }
|
||||
|
||||
/// <summary>
|
||||
/// Generates a media key from an entry key by sanitizing special characters
|
||||
@@ -105,19 +106,20 @@ public abstract class MediaVault : VaultIndexDirectory
|
||||
/// </summary>
|
||||
public class ImageVault : MediaVault
|
||||
{
|
||||
private ImageVault(string rootPath, VaultIndex index) : base(rootPath, index) { }
|
||||
private ImageVault(string rootPath, VaultIndex index, IndexFactoryService? factoryService = null)
|
||||
: base(rootPath, index, factoryService) { }
|
||||
|
||||
/// <summary>
|
||||
/// Factory method to create an ImageVault instance
|
||||
/// </summary>
|
||||
public static async Task<ImageVault?> FromAsync(string rootPath)
|
||||
public static async Task<ImageVault?> FromAsync(string rootPath, IndexFactoryService? factoryService = null)
|
||||
{
|
||||
var factoryService = new IndexFactoryService();
|
||||
var index = await factoryService.LoadOrCreateVaultIndexAsync(rootPath, MediaVaultType.Image);
|
||||
var factory = factoryService ?? new IndexFactoryService();
|
||||
var index = await factory.LoadOrCreateVaultIndexAsync(rootPath, MediaVaultType.Image);
|
||||
|
||||
if (index != null)
|
||||
{
|
||||
return new ImageVault(rootPath, (VaultIndex)index);
|
||||
return new ImageVault(rootPath, (VaultIndex)index, factory);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -126,16 +128,17 @@ public class ImageVault : MediaVault
|
||||
|
||||
public class AudioVault : MediaVault
|
||||
{
|
||||
private AudioVault(string rootPath, VaultIndex index) : base(rootPath, index) { }
|
||||
|
||||
public static async Task<AudioVault?> FromAsync(string rootPath)
|
||||
private AudioVault(string rootPath, VaultIndex index, IndexFactoryService? factoryService = null)
|
||||
: base(rootPath, index, factoryService) { }
|
||||
|
||||
public static async Task<AudioVault?> FromAsync(string rootPath, IndexFactoryService? factoryService = null)
|
||||
{
|
||||
var factoryService = new IndexFactoryService();
|
||||
var index = await factoryService.LoadOrCreateVaultIndexAsync(rootPath, MediaVaultType.Audio);
|
||||
var factory = factoryService ?? new IndexFactoryService();
|
||||
var index = await factory.LoadOrCreateVaultIndexAsync(rootPath, MediaVaultType.Audio);
|
||||
|
||||
if (index != null)
|
||||
{
|
||||
return new AudioVault(rootPath, (VaultIndex)index);
|
||||
return new AudioVault(rootPath, (VaultIndex)index, factory);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using DeepDrftContent.Services.FileDatabase.Abstractions;
|
||||
using DeepDrftContent.Services.FileDatabase.Models;
|
||||
using DeepDrftContent.Services.FileDatabase.Models;
|
||||
|
||||
namespace DeepDrftContent.Services.FileDatabase.Services;
|
||||
|
||||
@@ -8,10 +7,13 @@ namespace DeepDrftContent.Services.FileDatabase.Services;
|
||||
/// </summary>
|
||||
public static class MediaVaultFactory
|
||||
{
|
||||
private static readonly IMediaTypeRegistry _registry = new SimpleMediaTypeRegistry();
|
||||
|
||||
public static async Task<MediaVault?> From(string rootPath, MediaVaultType mediaType)
|
||||
public static async Task<MediaVault?> From(string rootPath, MediaVaultType mediaType, IndexFactoryService? factoryService = null)
|
||||
{
|
||||
return await _registry.CreateVaultAsync(mediaType, rootPath);
|
||||
return mediaType switch
|
||||
{
|
||||
MediaVaultType.Image => await ImageVault.FromAsync(rootPath, factoryService),
|
||||
MediaVaultType.Audio => await AudioVault.FromAsync(rootPath, factoryService),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -75,9 +75,10 @@ public class TrackService
|
||||
|
||||
return trackEntity;
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to add track: {ex.Message}", ex);
|
||||
Console.WriteLine($"TrackService.AddTrackFromWavAsync failed: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ public class TrackDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public required string EntryKey { get; set; }
|
||||
public string TrackName { get; set; }
|
||||
public string Artist { get; set; }
|
||||
public required string TrackName { get; set; }
|
||||
public required string Artist { get; set; }
|
||||
public string? Album { get; set; }
|
||||
public string? Genre { get; set; }
|
||||
public DateOnly? ReleaseDate { get; set; }
|
||||
|
||||
@@ -6,7 +6,7 @@ public class PagedResult<T>
|
||||
public int TotalCount { get; set; }
|
||||
public int Page { get; set; }
|
||||
public int PageSize { get; set; }
|
||||
public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize);
|
||||
public int TotalPages => PageSize > 0 ? (int)Math.Ceiling((double)TotalCount / PageSize) : 0;
|
||||
public bool HasNextPage => Page < TotalPages;
|
||||
public bool HasPreviousPage => Page > 1;
|
||||
|
||||
@@ -27,7 +27,7 @@ public class PagedResult<T>
|
||||
|
||||
public PagedResult(IEnumerable<T> items, int totalCount, int page, int pageSize)
|
||||
{
|
||||
Items = items.ToList() ?? new List<T>();
|
||||
Items = items.ToList();
|
||||
TotalCount = totalCount;
|
||||
Page = page;
|
||||
PageSize = pageSize;
|
||||
|
||||
Reference in New Issue
Block a user