using Harmonia.Core.Scanner; using Microsoft.EntityFrameworkCore; using SongModel = Harmonia.Core.Models.Song; namespace Harmonia.Core.Library; public interface IAudioLibrary { ValueTask AddLocationAsync(string path, CancellationToken cancellationToken); void RemoveLocation(string path); void RemoveLocations(string[] paths); void Scan(string path); void Scan(string[] paths); event EventHandler? LocationAdded; event EventHandler? LocationRemoved; event EventHandler? ScanStarted; event EventHandler? ScanFinished; } public abstract class AudioLibrary : IAudioLibrary { public event EventHandler? LocationAdded; public event EventHandler? LocationRemoved; public event EventHandler? ScanStarted; public event EventHandler? ScanFinished; protected abstract ValueTask IncludeLocationAsync(string path, CancellationToken cancellationToken); protected abstract ValueTask ScanLocationAsync(string path, CancellationToken cancellationToken); public async ValueTask AddLocationAsync(string path, CancellationToken cancellationToken) { if (await IncludeLocationAsync(path, cancellationToken) == false) return; LocationAdded?.Invoke(this, new EventArgs()); } public void RemoveLocation(string path) { RemoveLocations([path]); } public void RemoveLocations(string[] paths) { //foreach (string path in paths) //{ // //if (_locations.Contains(path) == false) // // continue; //} LocationRemoved?.Invoke(this, new EventArgs()); } public void Scan(string path) { Scan([path]); } public void Scan(string[] paths) { ScanStarted?.Invoke(this, new EventArgs()); ScanFinished?.Invoke(this, new EventArgs()); } } public class AudioDatabaseLibrary(IAudioFileScanner audioFileScanner, AudioLibraryContext context) : AudioLibrary { protected override async ValueTask IncludeLocationAsync(string path, CancellationToken cancellationToken) { if (CanAddLocation(path) == false) return false; Location location = GetLocation(path) ?? CreateLocation(path); await ScanLocationAsync(location, cancellationToken); return true; } private bool CanAddLocation(string path) { return context.Folders.Any(folder => folder.Name == path) == false; } private Location CreateLocation(string path) { Location location = new() { Name = path, Type = 0 }; context.Locations.Add(location); context.SaveChanges(); return location; } private Location? GetLocation(string path) { return context.Locations.FirstOrDefault(location => location.Name == path); } protected override async ValueTask ScanLocationAsync(string path, CancellationToken cancellationToken) { Location? location = GetLocation(path); if (location == null) return; await ScanLocationAsync(location, cancellationToken); } private async ValueTask ScanLocationAsync(Location location, CancellationToken cancellationToken) { SongModel[] songModels = await audioFileScanner.GetSongsFromPathAsync(location.Name, cancellationToken); AddFolders(location, songModels); AddSongs(songModels); // Remove old songs, and clear out folders that no longer hold songs } private void AddFolders(Location location, SongModel[] songModels) { string[] folderNames = [.. songModels.Select(songModel => Directory.GetParent(songModel.FileName)?.FullName ?? string.Empty) .Where(x => !string.IsNullOrWhiteSpace(x)) .Distinct()]; Folder[] folders = [.. context.Folders.Where(folder => folder.Location == location)]; } private void AddSongs(SongModel[] songModels) { foreach (SongModel songModel in songModels) { Folder? folder = context.Folders.FirstOrDefault(x => x.Name == songModel.FileDirectory); if (folder == null) continue; Song song = AddOrUpdateSong(songModel, folder); AddOrUpdateSongTag(songModel, song); } } private Song AddOrUpdateSong(SongModel songModel, Folder folder) { string shortFileName = Path.GetFileName(songModel.FileName); var song = context.Songs.FirstOrDefault(x => x.Folder == folder && string.Equals(x.FileName, shortFileName, StringComparison.OrdinalIgnoreCase)); if (song == null) { song = new() { FileName = shortFileName, Folder = folder }; context.Songs.Add(song); } song.Size = (long)songModel.Size; song.Duration = songModel.Length.Ticks; song.Modified = songModel.LastModified; song.BitRate = (int)songModel.BitRate; song.SampleRate = (int)songModel.SampleRate; return song; } private SongTag AddOrUpdateSongTag(SongModel songModel, Song song) { var songTag = context.SongTags.FirstOrDefault(x => x.Song == song); if (songTag == null) { songTag = new SongTag() { Song = song }; context.SongTags.Add(songTag); } songTag.Title = songModel.Title; songTag.Album = songModel.Album; songTag.Artist = string.Join(";", songModel.Artists); songTag.AlbumArtist = string.Join(";", songModel.AlbumArtists); songTag.TrackNumber = songModel.TrackNumber; songTag.DiscNumber = songModel.DiscNumber; songTag.Genre = songModel.Genre; songTag.ReleaseDate = songModel.Year != null ? new DateTime(songModel.Year.Value, 1, 1) : null; return songTag; } }