From dfdf514fe24251398d9eccdd4bf0b51fcf7e2b4f Mon Sep 17 00:00:00 2001 From: Brian Bicknell Date: Wed, 26 Feb 2025 19:07:29 -0500 Subject: [PATCH] Updated audio image extraction logic. Added caching classes. --- .../Caching/AudioImageMemoryCache.cs | 36 +++++++ Harmonia.Core/Caching/Cache.cs | 40 ++++++++ Harmonia.Core/Caching/IAudioImageCache.cs | 9 ++ Harmonia.Core/Caching/ICache.cs | 7 ++ Harmonia.Core/Caching/MemoryCache.cs | 43 +++++++++ .../Extensions/ServiceCollectionExtensions.cs | 25 +++++ Harmonia.Core/Harmonia.Core.csproj | 3 + Harmonia.Core/Imaging/AudioImageExtractor.cs | 96 +++++++++++++++++-- Harmonia.Core/Imaging/IAudioImageExtractor.cs | 4 +- Harmonia.Core/Imaging/SongPictureInfo.cs | 4 +- Harmonia.Core/Scanner/AudioFileScanner.cs | 6 +- 11 files changed, 259 insertions(+), 14 deletions(-) create mode 100644 Harmonia.Core/Caching/AudioImageMemoryCache.cs create mode 100644 Harmonia.Core/Caching/Cache.cs create mode 100644 Harmonia.Core/Caching/IAudioImageCache.cs create mode 100644 Harmonia.Core/Caching/ICache.cs create mode 100644 Harmonia.Core/Caching/MemoryCache.cs create mode 100644 Harmonia.Core/Extensions/ServiceCollectionExtensions.cs diff --git a/Harmonia.Core/Caching/AudioImageMemoryCache.cs b/Harmonia.Core/Caching/AudioImageMemoryCache.cs new file mode 100644 index 0000000..e671863 --- /dev/null +++ b/Harmonia.Core/Caching/AudioImageMemoryCache.cs @@ -0,0 +1,36 @@ +using Harmonia.Core.Imaging; +using Harmonia.Core.Models; + +namespace Harmonia.Core.Caching; + +public class AudioImageMemoryCache(IAudioImageExtractor audioImageExtractor) : MemoryCache, IAudioImageCache +{ + protected override object? GetKey(Song key) + { + if (string.IsNullOrWhiteSpace(key.ImageHash) == false) + { + return key.ImageHash; + } + else if (string.IsNullOrWhiteSpace(key.ImageName) == false) + { + return key.ImageName; + } + + return null; + } + + protected override SongPictureInfo? Fetch(Song key) + { + return audioImageExtractor.ExtractImage(key.FileName); + } + + protected override void AddToCache(object key, SongPictureInfo entry) + { + base.AddToCache(key, entry); + } + + protected override long GetEntrySize(SongPictureInfo entry) + { + return entry.Size; + } +} \ No newline at end of file diff --git a/Harmonia.Core/Caching/Cache.cs b/Harmonia.Core/Caching/Cache.cs new file mode 100644 index 0000000..721413d --- /dev/null +++ b/Harmonia.Core/Caching/Cache.cs @@ -0,0 +1,40 @@ +namespace Harmonia.Core.Caching; + +public abstract class Cache : ICache where TKey : notnull +{ + protected abstract TValue? TryGetValue(object key); + protected abstract TValue? Fetch(TKey key); + protected abstract void AddToCache(object key, TValue value); + + public TValue? Get(TKey key) + { + object? actualKey = GetKey(key); + + if (actualKey == null) + return default; + + return TryGetValue(actualKey) ?? Refresh(key); + } + + public TValue? Refresh(TKey key) + { + object? actualKey = GetKey(key); + + if (actualKey == null) + return default; + + TValue? value = Fetch(key); + + if (value == null) + return default; + + AddToCache(actualKey, value); + + return value; + } + + protected virtual object? GetKey(TKey key) + { + return key; + } +} \ No newline at end of file diff --git a/Harmonia.Core/Caching/IAudioImageCache.cs b/Harmonia.Core/Caching/IAudioImageCache.cs new file mode 100644 index 0000000..4c2ac60 --- /dev/null +++ b/Harmonia.Core/Caching/IAudioImageCache.cs @@ -0,0 +1,9 @@ +using Harmonia.Core.Imaging; +using Harmonia.Core.Models; + +namespace Harmonia.Core.Caching; + +public interface IAudioImageCache : ICache +{ + +} \ No newline at end of file diff --git a/Harmonia.Core/Caching/ICache.cs b/Harmonia.Core/Caching/ICache.cs new file mode 100644 index 0000000..584668b --- /dev/null +++ b/Harmonia.Core/Caching/ICache.cs @@ -0,0 +1,7 @@ +namespace Harmonia.Core.Caching; + +public interface ICache +{ + TValue? Get(TKey key); + TValue? Refresh(TKey key); +} \ No newline at end of file diff --git a/Harmonia.Core/Caching/MemoryCache.cs b/Harmonia.Core/Caching/MemoryCache.cs new file mode 100644 index 0000000..091e964 --- /dev/null +++ b/Harmonia.Core/Caching/MemoryCache.cs @@ -0,0 +1,43 @@ +using Microsoft.Extensions.Caching.Memory; + +namespace Harmonia.Core.Caching; + +public abstract class MemoryCache : Cache where TKey : notnull +{ + private readonly IMemoryCache _memoryCache; + + protected virtual long? SizeLimit => 2000000000; + protected virtual double CompactionPercentage => 0.2; + protected virtual TimeSpan SlidingExpiration => TimeSpan.FromSeconds(600); + + public MemoryCache() + { + MemoryCacheOptions memoryCacheOptions = new() + { + SizeLimit = SizeLimit, + CompactionPercentage = CompactionPercentage + }; + + _memoryCache = new MemoryCache(memoryCacheOptions); + } + + protected override TValue? TryGetValue(object key) + { + _memoryCache.TryGetValue(key, out TValue? value); + + return value; + } + + protected override void AddToCache(object key, TValue entry) + { + long entrySize = GetEntrySize(entry); + + var cacheEntryOptions = new MemoryCacheEntryOptions() + .SetSize(entrySize) + .SetSlidingExpiration(SlidingExpiration); + + _memoryCache.Set(key, entry, cacheEntryOptions); + } + + protected abstract long GetEntrySize(TValue entry); +} \ No newline at end of file diff --git a/Harmonia.Core/Extensions/ServiceCollectionExtensions.cs b/Harmonia.Core/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..12feaab --- /dev/null +++ b/Harmonia.Core/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,25 @@ +using Harmonia.Core.Engine; +using Harmonia.Core.Imaging; +using Harmonia.Core.Player; +using Harmonia.Core.Playlists; +using Harmonia.Core.Scanner; +using Harmonia.Core.Tags; +using Microsoft.Extensions.DependencyInjection; + +namespace Harmonia.Core.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddHarmonia(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + return services; + } +} \ No newline at end of file diff --git a/Harmonia.Core/Harmonia.Core.csproj b/Harmonia.Core/Harmonia.Core.csproj index 63aedb5..7bef851 100644 --- a/Harmonia.Core/Harmonia.Core.csproj +++ b/Harmonia.Core/Harmonia.Core.csproj @@ -9,6 +9,9 @@ + + + diff --git a/Harmonia.Core/Imaging/AudioImageExtractor.cs b/Harmonia.Core/Imaging/AudioImageExtractor.cs index 48c63aa..3ade0dc 100644 --- a/Harmonia.Core/Imaging/AudioImageExtractor.cs +++ b/Harmonia.Core/Imaging/AudioImageExtractor.cs @@ -2,16 +2,58 @@ namespace Harmonia.Core.Imaging; -public class AudioImageExtractor(ITagResolver tagResolver) : IAudioImageExtractor +public class AudioImageExtractor : IAudioImageExtractor { - public SongPictureInfo ExtractImage(string fileName) - { - SongTagInfo songTagInfo = tagResolver.GetSongTagInfo(fileName); + private readonly ITagResolver _tagResolver; + private readonly string[] _searchPatterns; + private readonly string[] _imageExtensions; - return ExtractImage(fileName, songTagInfo); + public AudioImageExtractor(ITagResolver tagResolver) + { + _tagResolver = tagResolver; + _searchPatterns = GetSearchPatterns(); + _imageExtensions = GetImageExtensions(); } - public SongPictureInfo ExtractImage(string fileName, SongTagInfo songTagInfo) + protected virtual string[] GetSearchPatterns() + { + return + [ + @"*%filename%*", + @"*folder*", + @"*front*", + @"*cover*", + @"Cover\*folder*", + @"Cover\*front*", + @"Cover\*cover*", + @"Cover\*", + @"Scans\*folder*", + @"Scans\*front*", + @"Scans\*cover*", + @"Scans\*", + @"*" + ]; + } + + + protected virtual string[] GetImageExtensions() + { + return + [ + "jpg", + "jpeg", + "png" + ]; + } + + public SongPictureInfo? ExtractImage(string path) + { + SongTagInfo songTagInfo = _tagResolver.GetSongTagInfo(path); + + return ExtractImage(path, songTagInfo); + } + + public SongPictureInfo? ExtractImage(string path, SongTagInfo songTagInfo) { if (songTagInfo.FrontCover != null) { @@ -21,7 +63,47 @@ public class AudioImageExtractor(ITagResolver tagResolver) : IAudioImageExtracto } else { - return SongPictureInfo.FromFile(fileName); + string? imagePath = GetImagePath(path); + + if (string.IsNullOrWhiteSpace(imagePath)) + return null; + + return SongPictureInfo.FromFile(path); + } + } + + private string? GetImagePath(string path) + { + string[] fileNames; + string extension; + + foreach (string searchPattern in _searchPatterns) + { + fileNames = GetFilesFromDirectory(path, searchPattern); + + foreach (string fileName in fileNames) + { + extension = Path.GetExtension(fileName)?.Replace(".", "") ?? string.Empty; + + if (_imageExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase) == false) + continue; + + return fileName; + } + } + + return null; + } + + private static string[] GetFilesFromDirectory(string path, string searchPattern) + { + try + { + return Directory.GetFiles(path, searchPattern); + } + catch (DirectoryNotFoundException) + { + return []; } } } \ No newline at end of file diff --git a/Harmonia.Core/Imaging/IAudioImageExtractor.cs b/Harmonia.Core/Imaging/IAudioImageExtractor.cs index 6b2cb0d..8021bef 100644 --- a/Harmonia.Core/Imaging/IAudioImageExtractor.cs +++ b/Harmonia.Core/Imaging/IAudioImageExtractor.cs @@ -4,6 +4,6 @@ namespace Harmonia.Core.Imaging; public interface IAudioImageExtractor { - SongPictureInfo ExtractImage(string fileName); - SongPictureInfo ExtractImage(string fileName, SongTagInfo songTagInfo); + SongPictureInfo? ExtractImage(string path); + SongPictureInfo? ExtractImage(string path, SongTagInfo songTagInfo); } \ No newline at end of file diff --git a/Harmonia.Core/Imaging/SongPictureInfo.cs b/Harmonia.Core/Imaging/SongPictureInfo.cs index 62945ba..5ebd912 100644 --- a/Harmonia.Core/Imaging/SongPictureInfo.cs +++ b/Harmonia.Core/Imaging/SongPictureInfo.cs @@ -7,7 +7,7 @@ public class SongPictureInfo : IDisposable public required Stream Stream { get; init; } public string? ImageHash { get; init; } public required string ImageName { get; init; } - public ulong Size { get; init; } + public long Size { get; init; } private SongPictureInfo() { @@ -45,7 +45,7 @@ public class SongPictureInfo : IDisposable Stream = stream, ImageHash = imageHash, ImageName = imageName, - Size = (ulong)stream.Length + Size = stream.Length }; return songPictureInfo; diff --git a/Harmonia.Core/Scanner/AudioFileScanner.cs b/Harmonia.Core/Scanner/AudioFileScanner.cs index 527d275..55f918c 100644 --- a/Harmonia.Core/Scanner/AudioFileScanner.cs +++ b/Harmonia.Core/Scanner/AudioFileScanner.cs @@ -30,7 +30,7 @@ public class AudioFileScanner(ITagResolver tagResolver, IAudioImageExtractor aud { FileInfo fileInfo = new(fileName); SongTagInfo songTagInfo = tagResolver.GetSongTagInfo(fileName); - using SongPictureInfo songPictureInfo = audioImageExtractor.ExtractImage(fileName, songTagInfo); + using SongPictureInfo? songPictureInfo = audioImageExtractor.ExtractImage(fileName, songTagInfo); Song song = new() { @@ -48,8 +48,8 @@ public class AudioFileScanner(ITagResolver tagResolver, IAudioImageExtractor aud Genre = songTagInfo.Genre, BitRate = songTagInfo.BitRate, SampleRate = songTagInfo.SampleRate, - ImageName = songPictureInfo.ImageName, - ImageHash = songPictureInfo.ImageHash + ImageName = songPictureInfo?.ImageName, + ImageHash = songPictureInfo?.ImageHash }; return song;