// 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();
}
}
}