diff --git a/Harmonia.Core/Imaging/AudioImageExtractor.cs b/Harmonia.Core/Imaging/AudioImageExtractor.cs new file mode 100644 index 0000000..48c63aa --- /dev/null +++ b/Harmonia.Core/Imaging/AudioImageExtractor.cs @@ -0,0 +1,27 @@ +using Harmonia.Core.Tags; + +namespace Harmonia.Core.Imaging; + +public class AudioImageExtractor(ITagResolver tagResolver) : IAudioImageExtractor +{ + public SongPictureInfo ExtractImage(string fileName) + { + SongTagInfo songTagInfo = tagResolver.GetSongTagInfo(fileName); + + return ExtractImage(fileName, songTagInfo); + } + + public SongPictureInfo ExtractImage(string fileName, SongTagInfo songTagInfo) + { + if (songTagInfo.FrontCover != null) + { + using MemoryStream memoryStream = new(songTagInfo.FrontCover.Data); + + return SongPictureInfo.FromStream(memoryStream); + } + else + { + return SongPictureInfo.FromFile(fileName); + } + } +} \ No newline at end of file diff --git a/Harmonia.Core/Imaging/IAudioImageExtractor.cs b/Harmonia.Core/Imaging/IAudioImageExtractor.cs new file mode 100644 index 0000000..6b2cb0d --- /dev/null +++ b/Harmonia.Core/Imaging/IAudioImageExtractor.cs @@ -0,0 +1,9 @@ +using Harmonia.Core.Tags; + +namespace Harmonia.Core.Imaging; + +public interface IAudioImageExtractor +{ + SongPictureInfo ExtractImage(string fileName); + SongPictureInfo ExtractImage(string fileName, SongTagInfo songTagInfo); +} \ No newline at end of file diff --git a/Harmonia.Core/Imaging/SongPictureInfo.cs b/Harmonia.Core/Imaging/SongPictureInfo.cs new file mode 100644 index 0000000..62945ba --- /dev/null +++ b/Harmonia.Core/Imaging/SongPictureInfo.cs @@ -0,0 +1,59 @@ +using System.Security.Cryptography; + +namespace Harmonia.Core.Imaging; + +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; } + + private SongPictureInfo() + { + + } + + public static SongPictureInfo FromStream(Stream stream) + { + string imageHash = ComputeImageHash(stream); + + return GetSongPictureInfo(stream, "Embedded", imageHash); + } + + private static string ComputeImageHash(Stream stream) + { + using var md5 = MD5.Create(); + byte[] hash = md5.ComputeHash(stream); + + return Convert.ToHexStringLower(hash); + } + + public static SongPictureInfo FromFile(string fileName) + { + using FileStream fileStream = File.OpenRead(fileName); + + return GetSongPictureInfo(fileStream, fileName); + } + + private static SongPictureInfo GetSongPictureInfo(Stream stream, string imageName, string? imageHash = null) + { + stream.Seek(0, SeekOrigin.Begin); + + SongPictureInfo songPictureInfo = new() + { + Stream = stream, + ImageHash = imageHash, + ImageName = imageName, + Size = (ulong)stream.Length + }; + + return songPictureInfo; + } + + public void Dispose() + { + Stream.Dispose(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/Harmonia.Core/Scanner/AudioFileScanner.cs b/Harmonia.Core/Scanner/AudioFileScanner.cs new file mode 100644 index 0000000..527d275 --- /dev/null +++ b/Harmonia.Core/Scanner/AudioFileScanner.cs @@ -0,0 +1,57 @@ +using Harmonia.Core.Imaging; +using Harmonia.Core.Models; +using Harmonia.Core.Tags; + +namespace Harmonia.Core.Scanner; + +public class AudioFileScanner(ITagResolver tagResolver, IAudioImageExtractor audioImageExtractor) : IAudioFileScanner +{ + public Song[] GetSongs(string[] fileNames) + { + List songs = []; + + foreach (string fileName in fileNames) + { + if (string.IsNullOrWhiteSpace(fileName) || File.Exists(fileName) == false) + continue; + + Song? song = GetSong(fileName); + + if (song == null) + continue; + + songs.Add(song); + } + + return [.. songs]; + } + + private Song GetSong(string fileName) + { + FileInfo fileInfo = new(fileName); + SongTagInfo songTagInfo = tagResolver.GetSongTagInfo(fileName); + using SongPictureInfo songPictureInfo = audioImageExtractor.ExtractImage(fileName, songTagInfo); + + Song song = new() + { + FileName = fileName, + Size = (ulong)fileInfo.Length, + LastModified = fileInfo.LastWriteTime, + Title = songTagInfo.Title, + Album = songTagInfo.Album, + Artists = songTagInfo.Artists, + AlbumArtists = songTagInfo.AlbumArtists, + DiscNumber = songTagInfo.DiscNumber, + TrackNumber = songTagInfo.TrackNumber, + Length = songTagInfo.Length, + Year = songTagInfo.Year, + Genre = songTagInfo.Genre, + BitRate = songTagInfo.BitRate, + SampleRate = songTagInfo.SampleRate, + ImageName = songPictureInfo.ImageName, + ImageHash = songPictureInfo.ImageHash + }; + + return song; + } +} \ No newline at end of file diff --git a/Harmonia.Core/Scanner/IAudioFileScanner.cs b/Harmonia.Core/Scanner/IAudioFileScanner.cs new file mode 100644 index 0000000..0d21678 --- /dev/null +++ b/Harmonia.Core/Scanner/IAudioFileScanner.cs @@ -0,0 +1,8 @@ +using Harmonia.Core.Models; + +namespace Harmonia.Core.Scanner; + +public interface IAudioFileScanner +{ + Song[] GetSongs(string[] fileNames); +} \ No newline at end of file diff --git a/Harmonia.Core/Tags/SongTagInfo.cs b/Harmonia.Core/Tags/SongTagInfo.cs index 2d79b40..f55ca99 100644 --- a/Harmonia.Core/Tags/SongTagInfo.cs +++ b/Harmonia.Core/Tags/SongTagInfo.cs @@ -6,13 +6,17 @@ public class SongTagInfo public string? Album { get; set; } public List Artists { get; set; } = []; public List AlbumArtists { get; set; } = []; - public int? DiscNumber { get; set; } - public int? TrackNumber { get; set; } + public byte? DiscNumber { get; set; } + public byte? TrackNumber { get; set; } public TimeSpan Length { get; set; } = TimeSpan.Zero; - public int? Year { get; set; } + public ushort? Year { get; set; } public string? Genre { get; set; } - public int BitRate { get; set; } - public int SampleRate { get; set; } + public uint BitRate { get; set; } + public uint SampleRate { get; set; } public List Pictures { get; set; } = []; public Dictionary Other { get; set; } = []; + + public PictureTag? FrontCover => Pictures.Where(x => x.PictureType != PictureType.NotAPicture) + .OrderBy(x => x.PictureType == PictureType.FrontCover) + .FirstOrDefault(); } \ No newline at end of file diff --git a/Harmonia.Core/Tags/TagLibTagResolver.cs b/Harmonia.Core/Tags/TagLibTagResolver.cs index 053008b..c0f6384 100644 --- a/Harmonia.Core/Tags/TagLibTagResolver.cs +++ b/Harmonia.Core/Tags/TagLibTagResolver.cs @@ -8,30 +8,30 @@ public class TagLibTagResolver : ITagResolver using (var tagFile = TagLib.File.Create(fileName)) { - int? discNumber = null; - int? trackNumber = null; - int? year = null; + byte? discNumber = null; + byte? trackNumber = null; + ushort? year = null; if (tagFile.Tag.Track > 0) - trackNumber = Convert.ToInt32(tagFile.Tag.Track); + trackNumber = (byte)tagFile.Tag.Track; if (tagFile.Tag.Disc > 0) - discNumber = Convert.ToInt32(tagFile.Tag.Disc); + discNumber = (byte)tagFile.Tag.Disc; if (tagFile.Tag.Year > 0) - year = Convert.ToInt32(tagFile.Tag.Year); + year = (ushort)tagFile.Tag.Year; songTagInfo.Title = tagFile.Tag.Title; songTagInfo.Album = tagFile.Tag.Album; - songTagInfo.Artists = tagFile.Tag.Performers.Select(x => x.Trim()).ToList(); - songTagInfo.AlbumArtists = tagFile.Tag.AlbumArtists.Select(x => x.Trim()).ToList(); + songTagInfo.Artists = [.. tagFile.Tag.Performers.Select(x => x.Trim())]; + songTagInfo.AlbumArtists = [.. tagFile.Tag.AlbumArtists.Select(x => x.Trim())]; songTagInfo.DiscNumber = discNumber; songTagInfo.TrackNumber = trackNumber; songTagInfo.Length = tagFile.Properties.Duration; songTagInfo.Year = year; songTagInfo.Genre = tagFile.Tag.JoinedGenres; - songTagInfo.BitRate = tagFile.Properties.AudioBitrate; - songTagInfo.SampleRate = tagFile.Properties.AudioSampleRate; + songTagInfo.BitRate = (uint)tagFile.Properties.AudioBitrate; + songTagInfo.SampleRate = (uint)tagFile.Properties.AudioSampleRate; foreach (var picture in tagFile.Tag.Pictures) { diff --git a/Harmonia.Tests/AudioPlayerTests.cs b/Harmonia.Tests/AudioPlayerTests.cs index e462b22..7ed12d9 100644 --- a/Harmonia.Tests/AudioPlayerTests.cs +++ b/Harmonia.Tests/AudioPlayerTests.cs @@ -8,7 +8,7 @@ using Shouldly; namespace Harmonia.Tests; -public class TestAudioPlayer(IAudioEngine audioEngine, IPlaylistRepository playlistRepository) +internal class TestAudioPlayer(IAudioEngine audioEngine, IPlaylistRepository playlistRepository) : AudioPlayer(audioEngine, playlistRepository) { internal void SetPlaylist(Playlist playlist) @@ -18,10 +18,7 @@ public class TestAudioPlayer(IAudioEngine audioEngine, IPlaylistRepository playl internal int GetPlayingSongIndex() { - if (Playlist == null) - return -1; - - if (PlayingSong == null) + if (Playlist == null || PlayingSong == null) return -1; return Playlist.Songs.IndexOf(PlayingSong); @@ -68,11 +65,9 @@ public class AudioPlayerTests _playlistRepository = Substitute.For(); _playlistRepository.Get().Returns([playlist]); - //_playlistRepository.GetPlaylist(_playlistSongs[0]).Returns(playlist); _audioPlayer = new TestAudioPlayer(_audioEngine, _playlistRepository); _audioPlayer.SetPlaylist(playlist); - //await _audioPlayer.LoadAsync(_playlistSongs[0], PlaybackMode.LoadOnly); } [Theory]