From 71d64a9dcba5dfe552df7cc33b13012f2181e7af Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Thu, 20 Feb 2025 12:14:37 +0500 Subject: [PATCH 1/3] output config API & command line --- src/Client/ActiveOutputInfo.cs | 17 +++ src/Client/IPlayerClient.cs | 16 +++ src/Client/IPlayerQuery.cs | 6 + src/Client/OutputDeviceInfo.cs | 17 +++ src/Client/OutputTypeInfo.cs | 24 ++++ src/Client/OutputsInfo.cs | 24 ++++ src/Client/PlayerClient.cs | 14 +++ src/Client/PlayerQuery.cs | 12 ++ src/Client/PlayerQueryResult.cs | 5 + src/CommandLineTool/Commands/OutputCommand.cs | 107 ++++++++++++++++++ src/CommandLineTool/Program.cs | 1 + .../Services/ValueFormatter.cs | 5 + 12 files changed, 248 insertions(+) create mode 100644 src/Client/ActiveOutputInfo.cs create mode 100644 src/Client/OutputDeviceInfo.cs create mode 100644 src/Client/OutputTypeInfo.cs create mode 100644 src/Client/OutputsInfo.cs create mode 100644 src/CommandLineTool/Commands/OutputCommand.cs diff --git a/src/Client/ActiveOutputInfo.cs b/src/Client/ActiveOutputInfo.cs new file mode 100644 index 0000000..18bec31 --- /dev/null +++ b/src/Client/ActiveOutputInfo.cs @@ -0,0 +1,17 @@ +namespace Beefweb.Client; + +/// +/// Information about active output device. +/// +public sealed class ActiveOutputInfo +{ + /// + /// Output type type id. + /// + public string TypeId { get; set; } = null!; + + /// + /// Output device id. + /// + public string DeviceId { get; set; } = null!; +} diff --git a/src/Client/IPlayerClient.cs b/src/Client/IPlayerClient.cs index 7ca719f..0dc9598 100644 --- a/src/Client/IPlayerClient.cs +++ b/src/Client/IPlayerClient.cs @@ -369,6 +369,22 @@ ValueTask SortPlaylist( /// Request task. ValueTask SortPlaylistRandomly(PlaylistRef playlist, CancellationToken cancellationToken = default); + /// + /// Gets information about output devices. + /// + /// Cancellation token. + /// Request task. + ValueTask GetOutputs(CancellationToken cancellationToken = default); + + /// + /// Sets output device. + /// + /// Output type id. If this value is null, current output type is not changed. + /// Output device id. + /// Cancellation token. + /// Request task. + ValueTask SetOutputDevice(string? typeId, string deviceId, CancellationToken cancellationToken = default); + // File browser API /// diff --git a/src/Client/IPlayerQuery.cs b/src/Client/IPlayerQuery.cs index dd29c82..663a4a5 100644 --- a/src/Client/IPlayerQuery.cs +++ b/src/Client/IPlayerQuery.cs @@ -46,6 +46,12 @@ IPlayerQuery IncludePlaylistItems( /// Configured query. IPlayerQuery IncludePlayQueue(IReadOnlyList? columns = null); + /// + /// Configures query to include outputs configuration. + /// + /// Configured query. + IPlayerQuery IncludeOutputs(); + /// /// Executes this query and returns player information. /// diff --git a/src/Client/OutputDeviceInfo.cs b/src/Client/OutputDeviceInfo.cs new file mode 100644 index 0000000..2a26543 --- /dev/null +++ b/src/Client/OutputDeviceInfo.cs @@ -0,0 +1,17 @@ +namespace Beefweb.Client; + +/// +/// Information about output device. +/// +public sealed class OutputDeviceInfo +{ + /// + /// Output device id. + /// + public string Id { get; set; } = null!; + + /// + /// Output device name. + /// + public string Name { get; set; } = null!; +} diff --git a/src/Client/OutputTypeInfo.cs b/src/Client/OutputTypeInfo.cs new file mode 100644 index 0000000..ca6685b --- /dev/null +++ b/src/Client/OutputTypeInfo.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace Beefweb.Client; + +/// +/// Information about output device type. +/// +public sealed class OutputTypeInfo +{ + /// + /// Output type id. + /// + public string Id { get; set; } = null!; + + /// + /// Output type name. + /// + public string Name { get; set; } = null!; + + /// + /// Available output devices. + /// + public IList Devices { get; set; } = null!; +} diff --git a/src/Client/OutputsInfo.cs b/src/Client/OutputsInfo.cs new file mode 100644 index 0000000..4fd78b7 --- /dev/null +++ b/src/Client/OutputsInfo.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace Beefweb.Client; + +/// +/// Information about audio outputs. +/// +public sealed class OutputsInfo +{ + /// + /// Information about active output device. + /// + public ActiveOutputInfo Active { get; set; } = null!; + + /// + /// Available output device types. + /// + public IList Types { get; set; } = null!; + + /// + /// If true player supports multiple output types. + /// + public bool SupportsMultipleOutputTypes { get; set; } +} diff --git a/src/Client/PlayerClient.cs b/src/Client/PlayerClient.cs index 3bdb2aa..7724866 100644 --- a/src/Client/PlayerClient.cs +++ b/src/Client/PlayerClient.cs @@ -394,6 +394,20 @@ await _handler .ConfigureAwait(false); } + /// + public async ValueTask GetOutputs(CancellationToken cancellationToken = default) + { + var result = await _handler.Get("api/outputs", null, cancellationToken).ConfigureAwait(false); + return result.Outputs ?? throw PropertyIsNull("outputs"); + } + + /// + public async ValueTask SetOutputDevice(string? typeId, string deviceId, + CancellationToken cancellationToken = default) + { + await _handler.Post("api/outputs/active", new { typeId, deviceId }, cancellationToken).ConfigureAwait(false); + } + // File browser API /// diff --git a/src/Client/PlayerQuery.cs b/src/Client/PlayerQuery.cs index 0f94c00..8da00d1 100644 --- a/src/Client/PlayerQuery.cs +++ b/src/Client/PlayerQuery.cs @@ -14,6 +14,7 @@ internal sealed class PlayerQuery : IPlayerQuery private bool _includePlayQueue; private bool _includePlaylists; private bool _includePlaylistItems; + private bool _includeOutputs; private bool _hasPlaylistItemsParameters; private IReadOnlyList? _activeItemColumns; private IReadOnlyList? _playQueueColumns; @@ -63,6 +64,12 @@ public IPlayerQuery IncludePlayQueue(IReadOnlyList? columns = null) return this; } + public IPlayerQuery IncludeOutputs() + { + _includeOutputs = true; + return this; + } + public async ValueTask Execute(CancellationToken cancellationToken = default) { return await _handler @@ -123,6 +130,11 @@ private QueryParameterCollection BuildQuery(bool wantPlaylistItemsParameters) } } + if (_includeOutputs) + { + query["outputs"] = true; + } + if (query.Count == 0) { throw new InvalidOperationException( diff --git a/src/Client/PlayerQueryResult.cs b/src/Client/PlayerQueryResult.cs index ed699a1..f264438 100644 --- a/src/Client/PlayerQueryResult.cs +++ b/src/Client/PlayerQueryResult.cs @@ -27,4 +27,9 @@ public sealed class PlayerQueryResult /// Play queue contents. /// public IList? PlayQueue { get; set; } + + /// + /// Outputs configuration. + /// + public OutputsInfo? Outputs { get; set; } } diff --git a/src/CommandLineTool/Commands/OutputCommand.cs b/src/CommandLineTool/Commands/OutputCommand.cs new file mode 100644 index 0000000..e660702 --- /dev/null +++ b/src/CommandLineTool/Commands/OutputCommand.cs @@ -0,0 +1,107 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Beefweb.Client; +using Beefweb.CommandLineTool.Services; +using McMaster.Extensions.CommandLineUtils; + +namespace Beefweb.CommandLineTool.Commands; + +[Command("outputs", "out", Description = "Show information about output devices or change output device")] +public class OutputCommand(IClientProvider clientProvider, IConsole console, ITabularWriter writer) + : ServerCommandBase(clientProvider) +{ + [Option("-a|--all", Description = "Print all available output devices")] + public bool All { get; set; } + + [Option("-t|--set-type", Description = "Set new output type")] + public string? TypeId { get; set; } + + [Option("-d|--set-device", Description = "Set new output device")] + public string? DeviceId { get; set; } + + public override async Task OnExecuteAsync(CancellationToken ct) + { + await base.OnExecuteAsync(ct); + + var outputs = await Client.GetOutputs(ct); + + if (DeviceId != null) + { + await SetOutputDevice(outputs, ct); + return; + } + + if (TypeId != null) + { + throw new InvalidRequestException("Invalid option combination: --set-type requires --set-device."); + } + + if (All) + PrintAll(outputs); + else + PrintCurrent(outputs); + } + + private void PrintAll(OutputsInfo outputs) + { + foreach (var type in outputs.Types) + { + var isCurrenType = type.Id == outputs.Active.TypeId; + var rows = Enumerable.Empty(); + + if (outputs.SupportsMultipleOutputTypes) + { + rows = rows.Concat([ + [" ", type.Id, type.Name], + [" ", "---", "---"] + ]); + } + + rows = rows.Concat(type.Devices.Select(d => (string[]) [ + isCurrenType && d.Id == outputs.Active.DeviceId ? "*" : " ", + d.Id, + d.FormatDeviceName() + ])); + + writer.WriteTable(rows.ToList()); + console.WriteLine(); + } + } + + private void PrintCurrent(OutputsInfo outputs) + { + var currentType = outputs.Types.First(c => c.Id == outputs.Active.TypeId); + var currentDevice = currentType.Devices.First(c => c.Id == outputs.Active.DeviceId); + + var rows = Enumerable.Empty(); + + if (outputs.SupportsMultipleOutputTypes) + rows = rows.Append([currentType.Id, currentType.Name]); + + rows = rows.Append([currentDevice.Id, currentDevice.FormatDeviceName()]); + + writer.WriteTable(rows.ToList()); + } + + private async ValueTask SetOutputDevice(OutputsInfo outputs, CancellationToken ct) + { + OutputTypeInfo type; + + if (TypeId != null) + { + type = outputs.Types.FirstOrDefault(c => string.Equals(c.Id, TypeId, StringComparison.OrdinalIgnoreCase)) + ?? throw new InvalidRequestException($"Unknown output type: {TypeId}."); + } + else + { + type = outputs.Types.First(c => c.Id == outputs.Active.TypeId); + } + + var device = type.Devices.FirstOrDefault(c => string.Equals(c.Id, DeviceId, StringComparison.OrdinalIgnoreCase)) + ?? throw new InvalidRequestException($"Unknown output device: ${DeviceId}."); + + await Client.SetOutputDevice(type.Id, device.Id, ct); + } +} diff --git a/src/CommandLineTool/Program.cs b/src/CommandLineTool/Program.cs index 628747f..7b502c0 100644 --- a/src/CommandLineTool/Program.cs +++ b/src/CommandLineTool/Program.cs @@ -26,6 +26,7 @@ namespace Beefweb.CommandLineTool; [Subcommand(typeof(SortCommand))] [Subcommand(typeof(ServersCommand))] [Subcommand(typeof(PlaylistsCommand))] +[Subcommand(typeof(OutputCommand))] [Subcommand(typeof(QueueCommand))] [Subcommand(typeof(StatusCommand))] [Subcommand(typeof(NowPlayingCommand))] diff --git a/src/CommandLineTool/Services/ValueFormatter.cs b/src/CommandLineTool/Services/ValueFormatter.cs index 688eafc..ba3ac5e 100644 --- a/src/CommandLineTool/Services/ValueFormatter.cs +++ b/src/CommandLineTool/Services/ValueFormatter.cs @@ -66,6 +66,11 @@ public static string Format(this VolumeInfo volumeInfo) }; } + public static string FormatDeviceName(this OutputDeviceInfo device) + { + return device.Name.Replace("\n", " "); + } + public static string FormatAsTrackTime(this TimeSpan time) { return time.Ticks switch From c06dd089a44a930e614b71a68dba2194acee4eec Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Thu, 20 Feb 2025 12:15:42 +0500 Subject: [PATCH 2/3] fix typo --- src/CommandLineTool/Commands/OutputCommand.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CommandLineTool/Commands/OutputCommand.cs b/src/CommandLineTool/Commands/OutputCommand.cs index e660702..1b99daf 100644 --- a/src/CommandLineTool/Commands/OutputCommand.cs +++ b/src/CommandLineTool/Commands/OutputCommand.cs @@ -48,7 +48,7 @@ private void PrintAll(OutputsInfo outputs) { foreach (var type in outputs.Types) { - var isCurrenType = type.Id == outputs.Active.TypeId; + var isCurrentType = type.Id == outputs.Active.TypeId; var rows = Enumerable.Empty(); if (outputs.SupportsMultipleOutputTypes) @@ -60,7 +60,7 @@ private void PrintAll(OutputsInfo outputs) } rows = rows.Concat(type.Devices.Select(d => (string[]) [ - isCurrenType && d.Id == outputs.Active.DeviceId ? "*" : " ", + isCurrentType && d.Id == outputs.Active.DeviceId ? "*" : " ", d.Id, d.FormatDeviceName() ])); From e57497752998f27cdf2c32fc4dcc1465f4967603 Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Thu, 20 Feb 2025 12:28:16 +0500 Subject: [PATCH 3/3] minor fixing --- src/CommandLineTool/Commands/OutputCommand.cs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/CommandLineTool/Commands/OutputCommand.cs b/src/CommandLineTool/Commands/OutputCommand.cs index 1b99daf..f5d82fe 100644 --- a/src/CommandLineTool/Commands/OutputCommand.cs +++ b/src/CommandLineTool/Commands/OutputCommand.cs @@ -87,20 +87,13 @@ private void PrintCurrent(OutputsInfo outputs) private async ValueTask SetOutputDevice(OutputsInfo outputs, CancellationToken ct) { - OutputTypeInfo type; - - if (TypeId != null) - { - type = outputs.Types.FirstOrDefault(c => string.Equals(c.Id, TypeId, StringComparison.OrdinalIgnoreCase)) - ?? throw new InvalidRequestException($"Unknown output type: {TypeId}."); - } - else - { - type = outputs.Types.First(c => c.Id == outputs.Active.TypeId); - } + var type = TypeId != null + ? outputs.Types.FirstOrDefault(c => string.Equals(c.Id, TypeId, StringComparison.OrdinalIgnoreCase)) + ?? throw new InvalidRequestException($"Unknown output type: {TypeId}.") + : outputs.Types.First(c => c.Id == outputs.Active.TypeId); var device = type.Devices.FirstOrDefault(c => string.Equals(c.Id, DeviceId, StringComparison.OrdinalIgnoreCase)) - ?? throw new InvalidRequestException($"Unknown output device: ${DeviceId}."); + ?? throw new InvalidRequestException($"Unknown output device: {DeviceId}."); await Client.SetOutputDevice(type.Id, device.Id, ct); }