using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Diagnostics; using OwlCore.Events; using OwlCore.Extensions; using StrixMusic.Cores.Files.Services; using StrixMusic.Sdk.AppModels; using StrixMusic.Sdk.CoreModels; using StrixMusic.Sdk.Extensions; using StrixMusic.Sdk.FileMetadata; using StrixMusic.Sdk.FileMetadata.Models; using StrixMusic.Sdk.MediaPlayback; namespace StrixMusic.Cores.Files.Models { /// /// Wraps around to provide track information extracted from a file to the Strix SDK. /// public sealed class FilesCoreTrack : ICoreTrack { private readonly IFileMetadataManager? _fileMetadataManager; private TrackMetadata _trackMetadata; /// /// Initializes a new instance of the class. /// /// The source core. /// The track metadata to wrap around public FilesCoreTrack(ICore sourceCore, TrackMetadata trackMetadata) { SourceCore = sourceCore; _trackMetadata = trackMetadata; _fileMetadataManager = SourceCore.Cast().FileMetadataManager; AttachEvents(); } private void AttachEvents() { Guard.IsNotNull(_fileMetadataManager, nameof(_fileMetadataManager)); _fileMetadataManager.Tracks.MetadataUpdated += Tracks_MetadataUpdated; } private void DetachEvents() { Guard.IsNotNull(_fileMetadataManager, nameof(_fileMetadataManager)); _fileMetadataManager.Tracks.MetadataUpdated -= Tracks_MetadataUpdated; } private void Tracks_MetadataUpdated(object sender, IEnumerable e) { foreach (var metadata in e) { if (metadata.Id != Id) return; Guard.IsNotNull(metadata.ArtistIds, nameof(metadata.ArtistIds)); Guard.IsNotNull(metadata.ImageIds, nameof(metadata.ImageIds)); Guard.IsNotNull(metadata.Genres, nameof(metadata.Genres)); var previousData = _trackMetadata; _trackMetadata = metadata; Guard.IsNotNull(previousData.ArtistIds, nameof(previousData.ArtistIds)); Guard.IsNotNull(previousData.ImageIds, nameof(previousData.ImageIds)); Guard.IsNotNull(previousData.Genres, nameof(previousData.Genres)); if (metadata.Title != previousData.Title) NameChanged?.Invoke(this, Name); if (metadata.Description != previousData.Description) DescriptionChanged?.Invoke(this, Description); if (metadata.DiscNumber != previousData.DiscNumber) TrackNumberChanged?.Invoke(this, TrackNumber); if (!Equals(metadata.Language, previousData.Language)) LanguageChanged?.Invoke(this, Language); if (!ReferenceEquals(metadata.Lyrics, previousData.Lyrics)) LyricsChanged?.Invoke(this, Lyrics); if (metadata.TrackNumber != previousData.TrackNumber) TrackNumberChanged?.Invoke(this, TrackNumber); if (metadata.Duration != previousData.Duration) DurationChanged?.Invoke(this, Duration); if (metadata.ArtistIds.Count != previousData.ArtistIds.Count) ArtistItemsCountChanged?.Invoke(this, metadata.ArtistIds.Count); if (metadata.ImageIds.Count != previousData.ImageIds.Count) ImagesCountChanged?.Invoke(this, metadata.ImageIds.Count); _ = HandleImagesChanged(previousData.ImageIds, metadata.ImageIds); _ = HandleArtistsChanged(previousData.ArtistIds, metadata.ArtistIds); } } private async Task HandleArtistsChanged(HashSet oldArtistIds, HashSet newArtistIds) { Guard.IsNotNull(_fileMetadataManager, nameof(_fileMetadataManager)); if (oldArtistIds.OrderBy(s => s).SequenceEqual(newArtistIds.OrderBy(s => s))) { // Lists have identical content, so no items have changed. return; } var addedArtists = newArtistIds.Except(oldArtistIds); var removedArtists = oldArtistIds.Except(newArtistIds); if (oldArtistIds.Count != newArtistIds.Count) { ArtistItemsChanged?.Invoke(this, await TransformAsync(addedArtists), await TransformAsync(removedArtists)); ArtistItemsCountChanged?.Invoke(this, newArtistIds.Count); } async Task>> TransformAsync(IEnumerable ids) { var idArray = ids as string[] ?? ids.ToArray(); var collectionChangedItems = new List>(idArray.Length); foreach (var id in idArray) { var artist = await _fileMetadataManager.Artists.GetByIdAsync(id); Guard.IsNotNullOrWhiteSpace(artist?.Id, nameof(artist.Id)); collectionChangedItems.Add(new CollectionChangedItem(InstanceCache.Artists.GetOrCreate(artist.Id, SourceCore, artist), collectionChangedItems.Count)); } return collectionChangedItems; } } private async Task HandleImagesChanged(HashSet oldImageIds, HashSet newImageIds) { Guard.IsNotNull(_fileMetadataManager, nameof(_fileMetadataManager)); if (oldImageIds.OrderBy(s => s).SequenceEqual(newImageIds.OrderBy(s => s))) { // Lists have identical content, so no images have changed. return; } var addedImages = newImageIds.Except(oldImageIds); var removedImages = oldImageIds.Except(newImageIds); if (oldImageIds.Count != newImageIds.Count) { ImagesChanged?.Invoke(this, await TransformAsync(addedImages), await TransformAsync(removedImages)); ImagesCountChanged?.Invoke(this, newImageIds.Count); } async Task>> TransformAsync(IEnumerable ids) { var idArray = ids as string[] ?? ids.ToArray(); var collectionChangedItems = new List>(idArray.Length); foreach (var id in idArray) { var image = await _fileMetadataManager.Images.GetByIdAsync(id); Guard.IsNotNullOrWhiteSpace(image?.Id, nameof(image.Id)); collectionChangedItems.Add(new CollectionChangedItem(InstanceCache.Images.GetOrCreate(image.Id, SourceCore, image), collectionChangedItems.Count)); } return collectionChangedItems; } } /// public event EventHandler? AlbumChanged; /// public event EventHandler? TrackNumberChanged; /// public event EventHandler? LanguageChanged; /// public event EventHandler? LyricsChanged; /// public event EventHandler? IsExplicitChanged; /// public event EventHandler? PlaybackStateChanged; /// public event EventHandler? NameChanged; /// public event EventHandler? DescriptionChanged; /// public event EventHandler? DurationChanged; /// public event EventHandler? LastPlayedChanged; /// public event EventHandler? IsPlayArtistCollectionAsyncAvailableChanged; /// public event EventHandler? IsPauseArtistCollectionAsyncAvailableChanged; /// public event EventHandler? IsChangeNameAsyncAvailableChanged; /// public event EventHandler? IsChangeDescriptionAsyncAvailableChanged; /// public event EventHandler? IsChangeDurationAsyncAvailableChanged; /// public event EventHandler? ArtistItemsCountChanged; /// public event EventHandler? GenresCountChanged; /// public event EventHandler? ImagesCountChanged; /// public event EventHandler? UrlsCountChanged; /// public event CollectionChangedEventHandler? ArtistItemsChanged; /// public event CollectionChangedEventHandler? GenresChanged; /// public event CollectionChangedEventHandler? ImagesChanged; /// public event CollectionChangedEventHandler? UrlsChanged; /// public string Id => _trackMetadata.Id ?? string.Empty; /// public TrackType Type => TrackType.Song; /// public int TotalArtistItemsCount => _trackMetadata.ArtistIds?.Count ?? 0; /// public int TotalImageCount => _trackMetadata.ImageIds?.Count ?? 0; /// public int TotalGenreCount => _trackMetadata.Genres?.Count ?? 0; /// public int TotalUrlCount => 0; /// public ICoreAlbum? Album { get; } /// /// Is not passed into the constructor. Should be set on object creation. public int? TrackNumber => Convert.ToInt32(_trackMetadata.TrackNumber); /// public int? DiscNumber { get; } /// public CultureInfo? Language { get; } /// public ICoreLyrics? Lyrics => null; /// public bool IsExplicit => false; /// public ICore SourceCore { get; } /// /// The path to the playable music file on disk. /// public string? LocalTrackPath => _trackMetadata.Url; /// public string Name => _trackMetadata.Title ?? string.Empty; /// public string? Description => _trackMetadata.Description; /// public PlaybackState PlaybackState { get; } /// public TimeSpan Duration => _trackMetadata.Duration ?? new TimeSpan(0, 0, 0); /// public DateTime? LastPlayed { get; } /// public DateTime? AddedAt { get; } /// public ICorePlayableCollectionGroup? RelatedItems => null; /// public bool IsChangeAlbumAsyncAvailable => false; /// public bool IsChangeTrackNumberAsyncAvailable => false; /// public bool IsChangeLanguageAsyncAvailable => false; /// public bool IsChangeLyricsAsyncAvailable => false; /// public bool IsChangeIsExplicitAsyncAvailable => false; /// public bool IsPlayArtistCollectionAsyncAvailable => false; /// public bool IsPauseArtistCollectionAsyncAvailable => false; /// public bool IsChangeNameAsyncAvailable => false; /// public bool IsChangeDescriptionAsyncAvailable => false; /// public bool IsChangeDurationAsyncAvailable => false; /// public Task IsAddArtistItemAvailableAsync(int index, CancellationToken cancellationToken = default) { return Task.FromResult(false); } /// public Task IsAddImageAvailableAsync(int index, CancellationToken cancellationToken = default) { return Task.FromResult(false); } /// public Task IsAddGenreAvailableAsync(int index, CancellationToken cancellationToken = default) { return Task.FromResult(false); } /// public Task IsAddUrlAvailableAsync(int index, CancellationToken cancellationToken = default) { return Task.FromResult(false); } /// public Task IsRemoveArtistItemAvailableAsync(int index, CancellationToken cancellationToken = default) { return Task.FromResult(false); } /// public Task IsRemoveImageAvailableAsync(int index, CancellationToken cancellationToken = default) { return Task.FromResult(false); } /// public Task IsRemoveGenreAvailableAsync(int index, CancellationToken cancellationToken = default) { return Task.FromResult(false); } /// public Task IsRemoveUrlAvailableAsync(int index, CancellationToken cancellationToken = default) { return Task.FromResult(false); } /// public Task ChangeAlbumAsync(ICoreAlbum? albums, CancellationToken cancellationToken = default) { throw new NotSupportedException(); } /// public Task ChangeDescriptionAsync(string? description, CancellationToken cancellationToken = default) { throw new NotSupportedException(); } /// public Task ChangeDurationAsync(TimeSpan duration, CancellationToken cancellationToken = default) { throw new NotSupportedException(); } /// public Task ChangeIsExplicitAsync(bool isExplicit, CancellationToken cancellationToken = default) { throw new NotSupportedException(); } /// public Task ChangeLanguageAsync(CultureInfo language, CancellationToken cancellationToken = default) { throw new NotSupportedException(); } /// public Task ChangeLyricsAsync(ICoreLyrics? lyrics, CancellationToken cancellationToken = default) { throw new NotSupportedException(); } /// public Task ChangeNameAsync(string name, CancellationToken cancellationToken = default) { throw new NotSupportedException(); } /// public Task ChangeTrackNumberAsync(int? trackNumber, CancellationToken cancellationToken = default) { throw new NotSupportedException(); } /// A cancellation token that may be used to cancel the ongoing task. /// public Task PauseArtistCollectionAsync(CancellationToken cancellationToken = default) { throw new NotSupportedException(); } /// A cancellation token that may be used to cancel the ongoing task. /// public Task PlayArtistCollectionAsync(CancellationToken cancellationToken = default) { throw new NotSupportedException(); } /// public Task PlayArtistCollectionAsync(ICoreArtistCollectionItem artistItem, CancellationToken cancellationToken = default) { throw new NotSupportedException(); } /// public Task AddArtistItemAsync(ICoreArtistCollectionItem artist, int index, CancellationToken cancellationToken = default) { throw new NotSupportedException(); } /// public Task AddImageAsync(ICoreImage image, int index, CancellationToken cancellationToken = default) { throw new NotSupportedException(); } /// public Task AddGenreAsync(ICoreGenre genre, int index, CancellationToken cancellationToken = default) { throw new NotSupportedException(); } /// public Task AddUrlAsync(ICoreUrl image, int index, CancellationToken cancellationToken = default) { throw new NotSupportedException(); } /// public Task RemoveArtistItemAsync(int index, CancellationToken cancellationToken = default) { throw new NotSupportedException(); } /// public Task RemoveImageAsync(int index, CancellationToken cancellationToken = default) { throw new NotSupportedException(); } /// public Task RemoveGenreAsync(int index, CancellationToken cancellationToken = default) { throw new NotSupportedException(); } /// public Task RemoveUrlAsync(int index, CancellationToken cancellationToken = default) { throw new NotSupportedException(); } /// public async IAsyncEnumerable GetArtistItemsAsync(int limit, int offset, [EnumeratorCancellation] CancellationToken cancellationToken = default) { Guard.IsNotNull(_fileMetadataManager, nameof(_fileMetadataManager)); var artists = await _fileMetadataManager.Artists.GetArtistsByTrackId(Id, offset, limit); foreach (var artist in artists) { Guard.IsNotNullOrWhiteSpace(artist.Id, nameof(artist.Id)); yield return InstanceCache.Artists.GetOrCreate(artist.Id, SourceCore, artist); } } /// public async IAsyncEnumerable GetImagesAsync(int limit, int offset, [EnumeratorCancellation] CancellationToken cancellationToken = default) { Guard.IsNotNull(_fileMetadataManager, nameof(_fileMetadataManager)); if (_trackMetadata.ImageIds == null) yield break; foreach (var imageId in _trackMetadata.ImageIds) { var image = await _fileMetadataManager.Images.GetByIdAsync(imageId); Guard.IsNotNull(image, nameof(image)); yield return InstanceCache.Images.GetOrCreate(imageId, SourceCore, image); } } /// public async IAsyncEnumerable GetGenresAsync(int limit, int offset, [EnumeratorCancellation] CancellationToken cancellationToken = default) { foreach (var genre in _trackMetadata.Genres ?? Enumerable.Empty()) { yield return new FilesCoreGenre(SourceCore, genre); } await Task.CompletedTask; } /// public IAsyncEnumerable GetUrlsAsync(int limit, int offset, CancellationToken cancellationToken = default) { return AsyncEnumerable.Empty(); } /// public ValueTask DisposeAsync() { DetachEvents(); return default; } } }