// 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.Linq; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Diagnostics; using StrixMusic.Sdk.AppModels; using StrixMusic.Sdk.BaseModels; using StrixMusic.Sdk.CoreModels; using StrixMusic.Sdk.Extensions; namespace StrixMusic.Sdk.MediaPlayback { /// /// Test /// public partial class PlaybackHandlerService { /// public async Task PlayAsync(ITrackCollection trackCollection, IPlayableBase context, CancellationToken cancellationToken = default) { var apiTracks = await trackCollection.GetTracksAsync(1, 0, cancellationToken).ToListAsync(cancellationToken); Guard.HasSizeGreaterThan(apiTracks, 0, nameof(apiTracks)); await PlayAsync(apiTracks[0], trackCollection, context, cancellationToken); } /// public async Task PlayAsync(ITrack track, ITrackCollection trackCollection, IPlayableBase context, CancellationToken cancellationToken = default) { var canPlay = await PrepareToPlayCollection(cancellationToken); if (!canPlay) return; ClearPrevious(); ClearNext(); await AddTrackCollectionToQueue(track, trackCollection, cancellationToken: cancellationToken); if (ShuffleState) ShuffleOnInternal(); CurrentItemContext = context; await PlayFromNext(0, cancellationToken); } /// public async Task PlayAsync(IArtistCollection artistCollection, IPlayableBase context, CancellationToken cancellationToken = default) { var apiArtists = await artistCollection.GetArtistItemsAsync(1, 0, cancellationToken).ToListAsync(cancellationToken); Guard.HasSizeGreaterThan(apiArtists, 0, nameof(apiArtists)); await PlayAsync(apiArtists[0], artistCollection, context, cancellationToken); } /// public async Task PlayAsync(IArtistCollectionItem artistCollectionItem, IArtistCollection artistCollection, IPlayableBase context, CancellationToken cancellationToken = default) { var canPlay = await PrepareToPlayCollection(cancellationToken); if (!canPlay) { await artistCollection.PlayArtistCollectionAsync(artistCollectionItem, cancellationToken); return; } ClearPrevious(); ClearNext(); await AddArtistCollectionToQueue(artistCollectionItem, artistCollection, cancellationToken); if (ShuffleState) ShuffleOnInternal(); CurrentItemContext = context; await PlayFromNext(0, cancellationToken); } /// public async Task PlayAsync(IPlaylistCollection playlistCollection, IPlayableBase context, CancellationToken cancellationToken = default) { var apiPlaylists = await playlistCollection.GetPlaylistItemsAsync(1, 0, cancellationToken).ToListAsync(cancellationToken); Guard.HasSizeGreaterThan(apiPlaylists, 0, nameof(apiPlaylists)); await PlayAsync(apiPlaylists[0], playlistCollection, context, cancellationToken); } /// public async Task PlayAsync(IAlbumCollection albumCollection, IPlayableBase context, CancellationToken cancellationToken = default) { var apiItems = await albumCollection.GetAlbumItemsAsync(1, 0, cancellationToken).ToListAsync(cancellationToken); Guard.HasSizeGreaterThan(apiItems, 0, nameof(apiItems)); await PlayAsync(apiItems[0], albumCollection, context, cancellationToken); } /// public async Task PlayAsync(IAlbumCollectionItem albumCollectionItem, IAlbumCollection albumCollection, IPlayableBase context, CancellationToken cancellationToken = default) { var canPlay = await PrepareToPlayCollection(cancellationToken); if (!canPlay) { await albumCollection.PlayAlbumCollectionAsync(albumCollectionItem, cancellationToken); return; } ClearPrevious(); ClearNext(); await AddAlbumCollectionToQueue(albumCollectionItem, albumCollection, cancellationToken); if (ShuffleState) ShuffleOnInternal(); CurrentItemContext = context; await PlayFromNext(0, cancellationToken); } /// public async Task PlayAsync(IPlaylistCollectionItem playlistCollectionItem, IPlaylistCollection playlistCollection, IPlayableBase context, CancellationToken cancellationToken = default) { var canPlay = await PrepareToPlayCollection(cancellationToken); if (!canPlay) { await playlistCollection.PlayPlaylistCollectionAsync(playlistCollectionItem, cancellationToken); return; } ClearPrevious(); ClearNext(); await AddPlaylistCollectionToQueue(playlistCollectionItem, playlistCollection, cancellationToken); if (ShuffleState) ShuffleOnInternal(); CurrentItemContext = context; await PlayFromNext(0, cancellationToken); } /// /// Common tasks done by all "Play" methods when playback is requested. /// /// True if playback should continue locally, false if playback should continue remotely. private async Task PrepareToPlayCollection(CancellationToken cancellationToken = default) { // Pause the active player first. if (ActiveDevice is not null) await ActiveDevice.PauseAsync(cancellationToken); // If there is no active device, activate the device used for local playback. if (ActiveDevice is null) { ActiveDevice = _localDevice; await _localDevice.SwitchToAsync(cancellationToken); } Guard.IsNotNull(ActiveDevice, nameof(ActiveDevice)); // If the active device is controlled remotely, the rest is handled there. return ActiveDevice.Type != DeviceType.Remote; } /// /// Adds the tracks in the collection to the queue. /// /// A target track to return an index for. /// The collection to iterate for adding to the queue. /// When adding to the queue, specify where to push the items to. /// A cancellation token that may be used to cancel the ongoing task. /// The index of the given in the . private Task AddTrackCollectionToQueue(ITrack track, ITrackCollection trackCollection, AddTrackPushTarget pushTarget = AddTrackPushTarget.Normal, CancellationToken cancellationToken = default) => AddTrackCollectionToQueue(track, trackCollection, 0, pushTarget, cancellationToken); /// /// Adds the tracks in the collection to the queue. /// /// A target track to return an index for. /// The collection to iterate for adding to the queue. /// The offset to add when adding tracks to . /// When adding to the queue, specify where to push the items to. /// A cancellation token that may be used to cancel the ongoing task. /// The index of the given in the . private async Task AddTrackCollectionToQueue(ITrack track, ITrackCollection trackCollection, int offset, AddTrackPushTarget pushTarget = AddTrackPushTarget.Normal, CancellationToken cancellationToken = default) { // Setup for local playback var trackPlaybackIndex = 0; var reachedTargetTrack = false; var i = -1; await foreach (var item in trackCollection.GetTracksAsync(trackCollection.TotalTrackCount, 0, cancellationToken)) { i++; var coreTrack = item.GetSources().First(x => x.Id == item.Id); var mediaSource = await coreTrack.SourceCore.GetMediaSourceAsync(coreTrack, cancellationToken); if (mediaSource is null) continue; if (item.Id == track.Id) { reachedTargetTrack = true; trackPlaybackIndex = i; } var playbackItem = new PlaybackItem { MediaConfig = mediaSource, Track = item }; switch (pushTarget) { case AddTrackPushTarget.Normal when reachedTargetTrack: InsertNext(i - trackPlaybackIndex, playbackItem); break; case AddTrackPushTarget.Normal: case AddTrackPushTarget.AllPrevious: PushPrevious(playbackItem); break; case AddTrackPushTarget.AllNext: InsertNext(i + offset, playbackItem); break; default: return ThrowHelper.ThrowArgumentOutOfRangeException(nameof(pushTarget)); } } Guard.IsTrue(reachedTargetTrack, nameof(reachedTargetTrack)); return trackPlaybackIndex; } /// /// Adds all albums in the given collection to the queue. /// /// The instance and index of the first playable track in the selected items within the entire . private async Task<(ITrack PlaybackTrack, int Index)> AddAlbumCollectionToQueue(IAlbumCollectionItem? albumCollectionItem, IAlbumCollection albumCollection, CancellationToken cancellationToken = default) { var itemIndex = 0; ITrack? playbackTrack = null; var foundItemTarget = false; var offset = 0; await foreach (var albumItem in albumCollection.GetAlbumItemsAsync(albumCollection.TotalAlbumItemsCount, offset, cancellationToken)) { if (albumItem is IAlbum album) { // Ignore albums without any tracks if (album.TotalTrackCount < 1) continue; var tracks = await album.GetTracksAsync(1, 0, cancellationToken).ToListAsync(cancellationToken);; var firstTrack = tracks[0]; if (albumItem.Id == albumCollectionItem?.Id && !foundItemTarget) { // Tracks are added to the queue of previous items until we reach the item the user wants to play. itemIndex = _prevItems.Count; foundItemTarget = true; playbackTrack = firstTrack; } if (foundItemTarget) { _ = await AddTrackCollectionToQueue(firstTrack, album, offset, foundItemTarget ? AddTrackPushTarget.AllNext : AddTrackPushTarget.AllPrevious, cancellationToken); offset = _nextItems.Count; } else { _ = await AddTrackCollectionToQueue(firstTrack, album, foundItemTarget ? AddTrackPushTarget.AllNext : AddTrackPushTarget.AllPrevious, cancellationToken); } } if (albumItem is IAlbumCollection albumCol) { _ = await AddAlbumCollectionToQueue(null, albumCol, cancellationToken); } } Guard.IsTrue(foundItemTarget, nameof(foundItemTarget)); Guard.IsNotNull(playbackTrack, nameof(playbackTrack)); return (playbackTrack, itemIndex); } /// /// Adds all artists in the given collection to the queue. /// /// The instance and index of the first playable track in the selected items within the entire . private async Task<(ITrack PlaybackTrack, int Index)> AddArtistCollectionToQueue(IArtistCollectionItem? artistCollectionItem, IArtistCollection artistCollection, CancellationToken cancellationToken = default) { var itemIndex = 0; ITrack? playbackTrack = null; var foundItemTarget = false; var offset = 0; await foreach (var artistItem in artistCollection.GetArtistItemsAsync(artistCollection.TotalArtistItemsCount, offset, cancellationToken)) { switch (artistItem) { // Ignore artist without any tracks case IArtist { TotalTrackCount: < 1 }: continue; case IArtist artist: var tracks = await artist.GetTracksAsync(1, 0, cancellationToken).ToListAsync(cancellationToken); var firstTrack = tracks[0]; if (artistItem.Id == artistCollectionItem?.Id && !foundItemTarget) { // Tracks are added to the queue of previous items until we reach the item the user wants to play. itemIndex = _prevItems.Count; foundItemTarget = true; playbackTrack = firstTrack; } if (foundItemTarget) { _ = await AddTrackCollectionToQueue(firstTrack, artist, offset, foundItemTarget ? AddTrackPushTarget.AllNext : AddTrackPushTarget.AllPrevious, cancellationToken); offset = _nextItems.Count; } else { _ = await AddTrackCollectionToQueue(firstTrack, artist, foundItemTarget ? AddTrackPushTarget.AllNext : AddTrackPushTarget.AllPrevious, cancellationToken); } break; case IArtistCollection artistCol: _ = await AddArtistCollectionToQueue(null, artistCol, cancellationToken); break; } } Guard.IsTrue(foundItemTarget, nameof(foundItemTarget)); Guard.IsNotNull(playbackTrack, nameof(playbackTrack)); return (playbackTrack, itemIndex); } /// /// Adds all playlists in the given collection to the queue. /// /// The instance and index of the first playable track in the selected items within the entire . private async Task<(ITrack PlaybackTrack, int Index)> AddPlaylistCollectionToQueue(IPlaylistCollectionItem? playlistCollectionItem, IPlaylistCollection playlistCollection, CancellationToken cancellationToken = default) { var itemIndex = 0; ITrack? playbackTrack = null; var foundItemTarget = false; var offset = 0; await foreach (var playlistItem in playlistCollection.GetPlaylistItemsAsync(playlistCollection.TotalPlaylistItemsCount, offset, cancellationToken)) { if (playlistItem is IPlaylist playlist) { // Ignore artist without any tracks if (playlist.TotalTrackCount < 1) continue; var tracks = await playlist.GetTracksAsync(1, 0, cancellationToken).ToListAsync(cancellationToken); var firstTrack = tracks[0]; if (playlistItem.Id == playlistCollectionItem?.Id && !foundItemTarget) { // Tracks are added to the queue of previous items until we reach the item the user wants to play. itemIndex = _prevItems.Count; foundItemTarget = true; playbackTrack = firstTrack; } if (foundItemTarget) { _ = await AddTrackCollectionToQueue(firstTrack, playlist, offset, foundItemTarget ? AddTrackPushTarget.AllNext : AddTrackPushTarget.AllPrevious, cancellationToken); offset = _nextItems.Count; } else { _ = await AddTrackCollectionToQueue(firstTrack, playlist, foundItemTarget ? AddTrackPushTarget.AllNext : AddTrackPushTarget.AllPrevious, cancellationToken); } } if (playlistItem is IPlaylistCollection playlistCol) { _ = await AddPlaylistCollectionToQueue(null, playlistCol, cancellationToken); } } Guard.IsTrue(foundItemTarget, nameof(foundItemTarget)); Guard.IsNotNull(playbackTrack, nameof(playbackTrack)); return (playbackTrack, itemIndex); } private enum AddTrackPushTarget : byte { Normal = 0, AllPrevious = 1, AllNext = 2, } } }