// Copyright (c) Arlo Godfrey. All Rights Reserved. // Licensed under the GNU Lesser General Public License, Version 3.0 with additional terms. // See the LICENSE, LICENSE.LESSER and LICENSE.ADDITIONAL files in the project root for more information. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Diagnostics; using Newtonsoft.Json; using OwlCore; using OwlCore.AbstractStorage; using OwlCore.Extensions; using StrixMusic.Sdk.FileMetadata.Models; namespace StrixMusic.Sdk.FileMetadata.Repositories { /// /// The service that helps in interacting with artist information. /// public sealed class ArtistRepository : IArtistRepository { private const string ARTIST_DATA_FILENAME = "ArtistMeta.bin"; private readonly ConcurrentDictionary _inMemoryMetadata; private readonly SemaphoreSlim _storageMutex; private readonly SemaphoreSlim _initMutex; private readonly string _debouncerId; private IFolderData? _folderData; /// /// Creates a new instance of . /// internal ArtistRepository() { _inMemoryMetadata = new ConcurrentDictionary(); _storageMutex = new SemaphoreSlim(1, 1); _initMutex = new SemaphoreSlim(1, 1); _debouncerId = Guid.NewGuid().ToString(); } /// public async Task InitAsync(CancellationToken cancellationToken = default) { using var initMutexReleaseRegistration = cancellationToken.Register(() => _initMutex.Release()); await _initMutex.WaitAsync(cancellationToken); if (IsInitialized) { _initMutex.Release(); return; } await LoadDataFromDisk(cancellationToken); IsInitialized = true; _initMutex.Release(); } /// public event EventHandler>? MetadataUpdated; /// public event EventHandler>? MetadataAdded; /// public event EventHandler>? MetadataRemoved; /// public bool IsInitialized { get; private set; } /// /// Sets the root folder to operate in when saving data. /// /// The root folder to work in. public void SetDataFolder(IFolderData rootFolder) { _folderData = rootFolder; } /// public Task GetItemCount() { return Task.FromResult(_inMemoryMetadata.Count); } /// public async Task AddOrUpdateAsync(params ArtistMetadata[] metadata) { await _storageMutex.WaitAsync(); var addedArtists = new List(); var updatedArtists = new List(); foreach (var item in metadata) { Guard.IsNotNullOrWhiteSpace(item.Id, nameof(item.Id)); var artistExists = true; var workingMetadata = _inMemoryMetadata.GetOrAdd(item.Id, key => { artistExists = false; return item; }); workingMetadata.AlbumIds ??= new HashSet(); workingMetadata.TrackIds ??= new HashSet(); workingMetadata.ImageIds ??= new HashSet(); item.AlbumIds ??= new HashSet(); item.TrackIds ??= new HashSet(); item.ImageIds ??= new HashSet(); Combine(workingMetadata.AlbumIds, item.AlbumIds); Combine(workingMetadata.TrackIds, item.TrackIds); Combine(workingMetadata.ImageIds, item.ImageIds); if (artistExists) updatedArtists.Add(workingMetadata); else addedArtists.Add(workingMetadata); } if (updatedArtists.Count > 0) MetadataUpdated?.Invoke(this, updatedArtists); if (addedArtists.Count > 0) MetadataAdded?.Invoke(this, addedArtists); _storageMutex.Release(); _ = CommitChangesAsync(); void Combine(HashSet originalData, HashSet newIds) { foreach (var newId in newIds.ToArray()) originalData.Add(newId); } } /// public async Task RemoveAsync(ArtistMetadata metadata) { Guard.IsNotNullOrWhiteSpace(metadata.Id, nameof(metadata.Id)); await _storageMutex.WaitAsync(); var removed = _inMemoryMetadata.TryRemove(metadata.Id, out _); _storageMutex.Release(); if (removed) { _ = CommitChangesAsync(); MetadataRemoved?.Invoke(this, metadata.IntoList()); } } /// public Task GetByIdAsync(string id) { _inMemoryMetadata.TryGetValue(id, out var metadata); return Task.FromResult(metadata); } /// public Task> GetItemsAsync(int offset, int limit) { var allArtists = _inMemoryMetadata.Values.ToList(); if (limit == -1) return Task.FromResult>(allArtists); // If the offset exceeds the number of items we have, return nothing. if (offset >= allArtists.Count) return Task.FromResult>(new List()); // If the total number of requested items exceeds the number of items we have, adjust the limit so it won't go out of range. if (offset + limit > allArtists.Count) limit = allArtists.Count - offset; return Task.FromResult>(allArtists.GetRange(offset, limit)); } /// public async Task> GetArtistsByAlbumId(string albumId, int offset, int limit) { var allArtists = await GetItemsAsync(offset, -1); var results = new List(); foreach (var item in allArtists) { Guard.IsNotNull(item.AlbumIds, nameof(item.AlbumIds)); Guard.IsGreaterThan(item.AlbumIds.Count, 0, nameof(item.AlbumIds.Count)); if (item.AlbumIds.Contains(albumId)) results.Add(item); } // If the offset exceeds the number of items we have, return nothing. if (offset >= results.Count) return new List(); // If the total number of requested items exceeds the number of items we have, adjust the limit so it won't go out of range. if (offset + limit > results.Count) limit = results.Count - offset; return results.GetRange(offset, limit).ToList(); } /// public async Task> GetArtistsByTrackId(string trackId, int offset, int limit) { var allArtists = await GetItemsAsync(0, -1); var results = new List(); foreach (var item in allArtists) { Guard.IsNotNull(item.TrackIds, nameof(item.TrackIds)); Guard.IsGreaterThan(item.TrackIds.Count, 0, nameof(item.TrackIds.Count)); if (item.TrackIds.Contains(trackId)) results.Add(item); } // If the offset exceeds the number of items we have, return nothing. if (offset >= results.Count) return new List(); // If the total number of requested items exceeds the number of items we have, adjust the limit so it won't go out of range. if (offset + limit > results.Count) limit = results.Count - offset; return results.GetRange(offset, limit).ToList(); } /// /// Gets the existing repo data stored on disk. /// /// The collection. private async Task LoadDataFromDisk(CancellationToken cancellationToken) { Guard.IsEmpty((ICollection>)_inMemoryMetadata, nameof(_inMemoryMetadata)); Guard.IsNotNull(_folderData, nameof(_folderData)); var fileData = await _folderData.CreateFileAsync(ARTIST_DATA_FILENAME, CreationCollisionOption.OpenIfExists); Guard.IsNotNull(fileData, nameof(fileData)); using var stream = await fileData.GetStreamAsync(FileAccessMode.ReadWrite); cancellationToken.ThrowIfCancellationRequested(); var bytes = await stream.ToBytesAsync(); cancellationToken.ThrowIfCancellationRequested(); if (bytes.Length == 0) return; var str = System.Text.Encoding.UTF8.GetString(bytes); var data = JsonConvert.DeserializeObject>(str); cancellationToken.ThrowIfCancellationRequested(); using var mutexReleaseOnCancelRegistration = cancellationToken.Register(() => _storageMutex.Release()); await _storageMutex.WaitAsync(cancellationToken); cancellationToken.ThrowIfCancellationRequested(); foreach (var item in data ?? Enumerable.Empty()) { Guard.IsNotNullOrWhiteSpace(item?.Id, nameof(item.Id)); if (!_inMemoryMetadata.TryAdd(item.Id, item)) ThrowHelper.ThrowInvalidOperationException($"Item already added to {nameof(_inMemoryMetadata)}"); } _storageMutex.Release(); } private async Task CommitChangesAsync() { if (!await Flow.Debounce(_debouncerId, TimeSpan.FromSeconds(4)) || _inMemoryMetadata.IsEmpty) return; await _storageMutex.WaitAsync(); Guard.IsNotNull(_folderData, nameof(_folderData)); var json = JsonConvert.SerializeObject(_inMemoryMetadata.Values.DistinctBy(x => x.Id).ToList()); var fileData = await _folderData.CreateFileAsync(ARTIST_DATA_FILENAME, CreationCollisionOption.OpenIfExists); await fileData.WriteAllBytesAsync(System.Text.Encoding.UTF8.GetBytes(json)); _storageMutex.Release(); } /// public void Dispose() { _initMutex.Dispose(); _storageMutex.Dispose(); } } }