Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/Client/ActiveOutputInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Beefweb.Client;

/// <summary>
/// Information about active output device.
/// </summary>
public sealed class ActiveOutputInfo
{
/// <summary>
/// Output type type id.
/// </summary>
public string TypeId { get; set; } = null!;

/// <summary>
/// Output device id.
/// </summary>
public string DeviceId { get; set; } = null!;
}
16 changes: 16 additions & 0 deletions src/Client/IPlayerClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,22 @@ ValueTask SortPlaylist(
/// <returns>Request task.</returns>
ValueTask SortPlaylistRandomly(PlaylistRef playlist, CancellationToken cancellationToken = default);

/// <summary>
/// Gets information about output devices.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Request task.</returns>
ValueTask<OutputsInfo> GetOutputs(CancellationToken cancellationToken = default);

/// <summary>
/// Sets output device.
/// </summary>
/// <param name="typeId">Output type id. If this value is null, current output type is not changed.</param>
/// <param name="deviceId">Output device id.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Request task.</returns>
ValueTask SetOutputDevice(string? typeId, string deviceId, CancellationToken cancellationToken = default);

// File browser API

/// <summary>
Expand Down
6 changes: 6 additions & 0 deletions src/Client/IPlayerQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ IPlayerQuery IncludePlaylistItems(
/// <returns>Configured query.</returns>
IPlayerQuery IncludePlayQueue(IReadOnlyList<string>? columns = null);

/// <summary>
/// Configures query to include outputs configuration.
/// </summary>
/// <returns>Configured query.</returns>
IPlayerQuery IncludeOutputs();

/// <summary>
/// Executes this query and returns player information.
/// </summary>
Expand Down
17 changes: 17 additions & 0 deletions src/Client/OutputDeviceInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Beefweb.Client;

/// <summary>
/// Information about output device.
/// </summary>
public sealed class OutputDeviceInfo
{
/// <summary>
/// Output device id.
/// </summary>
public string Id { get; set; } = null!;

/// <summary>
/// Output device name.
/// </summary>
public string Name { get; set; } = null!;
}
24 changes: 24 additions & 0 deletions src/Client/OutputTypeInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Collections.Generic;

namespace Beefweb.Client;

/// <summary>
/// Information about output device type.
/// </summary>
public sealed class OutputTypeInfo
{
/// <summary>
/// Output type id.
/// </summary>
public string Id { get; set; } = null!;

/// <summary>
/// Output type name.
/// </summary>
public string Name { get; set; } = null!;

/// <summary>
/// Available output devices.
/// </summary>
public IList<OutputDeviceInfo> Devices { get; set; } = null!;
}
24 changes: 24 additions & 0 deletions src/Client/OutputsInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Collections.Generic;

namespace Beefweb.Client;

/// <summary>
/// Information about audio outputs.
/// </summary>
public sealed class OutputsInfo
{
/// <summary>
/// Information about active output device.
/// </summary>
public ActiveOutputInfo Active { get; set; } = null!;

/// <summary>
/// Available output device types.
/// </summary>
public IList<OutputTypeInfo> Types { get; set; } = null!;

/// <summary>
/// If true player supports multiple output types.
/// </summary>
public bool SupportsMultipleOutputTypes { get; set; }
}
14 changes: 14 additions & 0 deletions src/Client/PlayerClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,20 @@ await _handler
.ConfigureAwait(false);
}

/// <inheritdoc />
public async ValueTask<OutputsInfo> GetOutputs(CancellationToken cancellationToken = default)
{
var result = await _handler.Get<PlayerQueryResult>("api/outputs", null, cancellationToken).ConfigureAwait(false);
return result.Outputs ?? throw PropertyIsNull("outputs");
}

/// <inheritdoc />
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

/// <inheritdoc />
Expand Down
12 changes: 12 additions & 0 deletions src/Client/PlayerQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>? _activeItemColumns;
private IReadOnlyList<string>? _playQueueColumns;
Expand Down Expand Up @@ -63,6 +64,12 @@ public IPlayerQuery IncludePlayQueue(IReadOnlyList<string>? columns = null)
return this;
}

public IPlayerQuery IncludeOutputs()
{
_includeOutputs = true;
return this;
}

public async ValueTask<PlayerQueryResult> Execute(CancellationToken cancellationToken = default)
{
return await _handler
Expand Down Expand Up @@ -123,6 +130,11 @@ private QueryParameterCollection BuildQuery(bool wantPlaylistItemsParameters)
}
}

if (_includeOutputs)
{
query["outputs"] = true;
}

if (query.Count == 0)
{
throw new InvalidOperationException(
Expand Down
5 changes: 5 additions & 0 deletions src/Client/PlayerQueryResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,9 @@ public sealed class PlayerQueryResult
/// Play queue contents.
/// </summary>
public IList<PlayQueueItemInfo>? PlayQueue { get; set; }

/// <summary>
/// Outputs configuration.
/// </summary>
public OutputsInfo? Outputs { get; set; }
}
100 changes: 100 additions & 0 deletions src/CommandLineTool/Commands/OutputCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
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 isCurrentType = type.Id == outputs.Active.TypeId;
var rows = Enumerable.Empty<string[]>();

if (outputs.SupportsMultipleOutputTypes)
{
rows = rows.Concat([
[" ", type.Id, type.Name],
[" ", "---", "---"]
]);
}

rows = rows.Concat(type.Devices.Select(d => (string[]) [
isCurrentType && 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<string[]>();

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)
{
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}.");

await Client.SetOutputDevice(type.Id, device.Id, ct);
}
}
1 change: 1 addition & 0 deletions src/CommandLineTool/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
Expand Down
5 changes: 5 additions & 0 deletions src/CommandLineTool/Services/ValueFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down