// 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.AbstractUI.Models; using OwlCore.Events; using OwlCore.Extensions; using OwlCore.Remoting; using StrixMusic.Sdk.AppModels; using StrixMusic.Sdk.CoreModels; using StrixMusic.Sdk.MediaPlayback; using StrixMusic.Sdk.Services; namespace StrixMusic.Sdk.Plugins.CoreRemote { /// /// Wraps around an instance of an to enable controlling it remotely, or takes a remotingId to control another instance remotely. /// /// /// Passing a core instance will enable remoting for the ENTIRE core, including library, search, playback, devices and all other feature. /// [RemoteOptions(RemotingDirection.Bidirectional)] public sealed class RemoteCore : ICore { private static readonly ConcurrentDictionary _hostCoreInstances = new ConcurrentDictionary(); private static readonly ConcurrentDictionary _clientCoreInstances = new ConcurrentDictionary(); private readonly MemberRemote _memberRemote; private readonly ICore? _core; private readonly object _devicesChangedLockObj = new object(); private readonly List _devices = new List(); private CoreState _coreState = CoreState.Unloaded; private string _instanceDescriptor = string.Empty; /// /// Creates a new instance of . /// [JsonConstructor] public RemoteCore(string instanceId) { if (!_clientCoreInstances.TryAdd(instanceId, this)) ThrowHelper.ThrowInvalidOperationException($"An instance with that ID already exists. Use RemoteCore.GetInstance(id) instead."); InstanceId = instanceId; // Dummy values to satisfy nullable. Will be overwritten remotely from other ctor. RecentlyPlayed = null!; Discoverables = null!; Pins = null!; Library = null!; AbstractConfigPanel = new AbstractUICollection("TemporaryCollection"); // Registration is set remotely, use placeholder data here. Registration = new CoreMetadata(string.Empty, string.Empty, new Uri("/", UriKind.Relative), sdkVer: new Version(0, 0, 0)); _memberRemote = new MemberRemote(this, $"{instanceId}.{nameof(RemoteCore)}", RemoteCoreMessageHandler.SingletonClient); } /// /// Wraps around and remotely relays events, property changes and method calls (with return data) from a core instance. /// /// public RemoteCore(ICore core) { Guard.IsNotNull(core, nameof(core)); if (!_hostCoreInstances.TryAdd(core.InstanceId, this)) ThrowHelper.ThrowInvalidOperationException($"An instance with that ID already exists. Use RemoteCore.GetInstance(id) instead."); _core = core; _memberRemote = new MemberRemote(this, $"{core.InstanceId}.{nameof(RemoteCore)}", RemoteCoreMessageHandler.SingletonHost); Library = new RemoteCoreLibrary(core.Library); if (core.RecentlyPlayed is not null) RecentlyPlayed = new RemoteCoreRecentlyPlayed(core.RecentlyPlayed); if (core.Pins is not null) Pins = new RemoteCorePlayableCollectionGroup(core.Pins); if (core.Discoverables is not null) Discoverables = new RemoteCoreDiscoverables(core.Discoverables); ChangeDevices(core.Devices.Select((x, i) => new CollectionChangedItem(x, i)).ToList(), new List>()); AttachEvents(core); AbstractConfigPanel = _core.AbstractConfigPanel; Registration = core.Registration; InstanceDescriptor = core.InstanceDescriptor; InstanceId = core.InstanceId; User = core.User; } /// /// Gets a created instance by instance ID. /// /// The core instance. /// public static RemoteCore GetInstance(string instanceId, RemotingMode mode) { if (mode is RemotingMode.Client) { if (!_clientCoreInstances.TryGetValue(instanceId, out var value)) return ThrowHelper.ThrowInvalidOperationException($"Could not find a registered {nameof(RemoteCore)} with {nameof(instanceId)} of {instanceId}"); return value; } if (mode is RemotingMode.Host) { if (!_hostCoreInstances.TryGetValue(instanceId, out var value)) return ThrowHelper.ThrowInvalidOperationException($"Could not find a registered {nameof(RemoteCore)} with {nameof(instanceId)} of {instanceId}"); return value; } return ThrowHelper.ThrowArgumentOutOfRangeException("Invalid remoting mode specified."); } private void AttachEvents(ICore core) { core.InstanceDescriptorChanged += OnInstanceDescriptorChanged; core.DevicesChanged += OnDevicesChanged; core.CoreStateChanged += OnCoreStateChanged; } private void DetachEvents(ICore core) { core.InstanceDescriptorChanged -= OnInstanceDescriptorChanged; core.DevicesChanged -= OnDevicesChanged; core.CoreStateChanged -= OnCoreStateChanged; } private void OnCoreStateChanged(object sender, CoreState e) => CoreState = e; private void OnInstanceDescriptorChanged(object sender, string e) { InstanceDescriptor = e; } private void OnDevicesChanged(object sender, IReadOnlyList> addedItems, IReadOnlyList> removedItems) { ChangeDevices(addedItems, removedItems); } [RemoteMethod, RemoteOptions(RemotingDirection.HostToClient)] private void ChangeDevices(IReadOnlyList> addedItems, IReadOnlyList> removedItems) { lock (_devicesChangedLockObj) { if (addedItems.Count + removedItems.Count == 0) return; var remoteAddedItems = addedItems.Select(x => new CollectionChangedItem(new RemoteCoreDevice(x.Data), x.Index)).ToList(); var remoteRemovedItems = removedItems.Select(x => new CollectionChangedItem(new RemoteCoreDevice(x.Data), x.Index)).ToList(); _devices.ChangeCollection(remoteAddedItems, remoteRemovedItems); DevicesChanged?.Invoke(this, remoteAddedItems, remoteRemovedItems); } } /// [RemoteProperty] public CoreMetadata Registration { get; set; } /// public string InstanceId { get; } /// public ICore SourceCore => this; /// [RemoteProperty] public CoreState CoreState { get => _coreState; set { _coreState = value; CoreStateChanged?.Invoke(this, value); } } /// [RemoteProperty] public string InstanceDescriptor { get => _instanceDescriptor; set { _instanceDescriptor = value; InstanceDescriptorChanged?.Invoke(this, value); } } /// [RemoteProperty] public AbstractUICollection AbstractConfigPanel { get; private set; } /// [RemoteProperty] public MediaPlayerType PlaybackType { get; private set; } /// [RemoteProperty] public ICoreUser? User { get; set; } /// [RemoteProperty] public IReadOnlyList Devices => _devices; /// [RemoteProperty, RemoteOptions(RemotingDirection.HostToClient)] public ICoreLibrary Library { get; set; } /// [RemoteProperty, RemoteOptions(RemotingDirection.HostToClient)] public ICoreSearch? Search { get; set; } /// [RemoteProperty, RemoteOptions(RemotingDirection.HostToClient)] public ICoreRecentlyPlayed? RecentlyPlayed { get; set; } /// [RemoteProperty, RemoteOptions(RemotingDirection.HostToClient)] public ICoreDiscoverables? Discoverables { get; set; } /// [RemoteProperty, RemoteOptions(RemotingDirection.HostToClient)] public ICorePlayableCollectionGroup? Pins { get; set; } /// public event EventHandler? CoreStateChanged; /// public event CollectionChangedEventHandler? DevicesChanged; /// public event EventHandler? AbstractConfigPanelChanged; /// public event EventHandler? InstanceDescriptorChanged; /// [RemoteMethod] public ValueTask DisposeAsync() { // Dispose any resources not known to the SDK. // Do not dispose Library, Devices, etc. manually. The SDK will dispose these for you. _clientCoreInstances.TryRemove(InstanceId, out _); return default; } /// public Task InitAsync(CancellationToken cancellationToken = default) => Task.Run(async () => { if (_memberRemote.Mode == RemotingMode.Host) return; await RemoteInitAsync(); IsInitialized = true; }); /// public bool IsInitialized { get; private set; } [RemoteMethod, RemoteOptions(RemotingDirection.ClientToHost)] private Task RemoteInitAsync() => Task.Run(async () => { if (_memberRemote.Mode == RemotingMode.Client) { await _memberRemote.RemoteWaitAsync(nameof(InitAsync)); return; } Guard.IsNotNull(_core, nameof(_core)); await _core.InitAsync(); await _memberRemote.RemoteReleaseAsync(nameof(InitAsync)); }); /// [RemoteMethod, RemoteOptions(RemotingDirection.ClientToHost)] public Task GetContextByIdAsync(string id, CancellationToken cancellationToken = default) => Task.Run(async () => { var methodCallToken = $"{nameof(GetContextByIdAsync)}.{id}"; if (_memberRemote.Mode == RemotingMode.Host) { Guard.IsNotNull(_core, nameof(_core)); var result = await _core.GetContextByIdAsync(id, cancellationToken); ICoreMember? remoteEnabledResult = result switch { ICore core => GetInstance(core.InstanceId, _memberRemote.Mode), ICoreAlbum album => new RemoteCoreAlbum(album), ICoreArtist artist => new RemoteCoreArtist(artist), ICoreTrack track => new RemoteCoreTrack(track), ICorePlaylist playlist => new RemoteCorePlaylist(playlist), ICoreDevice device => new RemoteCoreDevice(device), ICoreDiscoverables discoverables => new RemoteCoreDiscoverables(discoverables), ICoreImage image => new RemoteCoreImage(image), ICoreLibrary library => new RemoteCoreLibrary(library), ICoreRecentlyPlayed recentlyPlayed => new RemoteCoreRecentlyPlayed(recentlyPlayed), ICoreSearchHistory searchHistory => new RemoteCoreSearchHistory(searchHistory), ICorePlayableCollectionGroup collectionGroup => new RemoteCorePlayableCollectionGroup(collectionGroup), _ => throw new NotImplementedException(), }; return await _memberRemote.PublishDataAsync(methodCallToken, remoteEnabledResult); } else if (_memberRemote.Mode == RemotingMode.Client) { return await _memberRemote.ReceiveDataAsync(methodCallToken); } else { return null; } }); /// [RemoteMethod, RemoteOptions(RemotingDirection.ClientToHost)] public Task GetMediaSourceAsync(ICoreTrack track, CancellationToken cancellationToken = default) => Task.Run(async () => { var methodCallToken = $"{nameof(GetMediaSourceAsync)}.{track.Id}"; if (_memberRemote.Mode == RemotingMode.Host) { Guard.IsNotNull(_core, nameof(_core)); var result = await _core.GetMediaSourceAsync(track); return await _memberRemote.PublishDataAsync(methodCallToken, result); } else if (_memberRemote.Mode == RemotingMode.Client) { return await _memberRemote.ReceiveDataAsync(methodCallToken); } else { return null; } }); } }