Audio: let `audio-devices <device>` write the config

The audio-devices command only listed. Give it an optional positional
argument: with none it lists as before; with a device name (or
case-insensitive fragment) it writes that into [audio] device in
crabidy-server.toml and then lists, so the same command both configures
and confirms (the chosen device is marked with *). If the fragment
matches no current output device it still writes but warns, mirroring
the server's startup fallback -- so a typo is caught here, not as silent
output.

Plumbing: AudioDevices carries an AudioDevicesArgs { device: Option }
on both `crabidy-server` and `cbd`; cli::audio_devices takes the option
and, when set, loads/updates/stores the settings via the existing
ServerSettings writer. README shows the set form.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Test User 2026-07-23 22:44:46 +02:00
parent 4c0c5f1401
commit 28a4c155d0
5 changed files with 62 additions and 15 deletions

View File

@ -171,17 +171,24 @@ Audio output devices (* = selected by the current config):
... ...
``` ```
Then pin one in `crabidy-server.toml` — the value is matched Then pin one by passing it to the same command — the value is matched
case-insensitively as a substring of the name, so a memorable fragment is case-insensitively as a substring of the name, so a memorable fragment is
enough — and restart the server: enough — and restart the server:
```console
$ crabidy-server audio-devices Headphones
Set [audio] device = "Headphones" in .../crabidy-server.toml
```
That writes `[audio] device` for you; you can also edit it by hand:
```toml ```toml
[audio] [audio]
device = "Headphones" device = "Headphones"
``` ```
If the name matches nothing, the server logs a warning and falls back to If the name matches nothing, both the command and the server warn, and the
the system default. server falls back to the system default.
## Command line ## Command line

View File

@ -179,6 +179,15 @@ pub struct CompletionsArgs {
pub shell: clap_complete::Shell, pub shell: clap_complete::Shell,
} }
/// `audio-devices [device]`: list the output devices, or — with `device` — set
/// `[audio] device` in `crabidy-server.toml`.
#[derive(Debug, Args)]
pub struct AudioDevicesArgs {
/// A name (or case-insensitive fragment) of an output device to write into
/// `[audio] device`. Omit to just list the available devices.
pub device: Option<String>,
}
/// Subcommands of `crabidy-server`. /// Subcommands of `crabidy-server`.
#[derive(Debug, Subcommand)] #[derive(Debug, Subcommand)]
pub enum ServerCommand { pub enum ServerCommand {
@ -195,8 +204,8 @@ pub enum ServerCommand {
/// Playback / global operations against a running server. /// Playback / global operations against a running server.
#[command(subcommand)] #[command(subcommand)]
Global(GlobalCmd), Global(GlobalCmd),
/// List the audio output devices (to pick `[audio] device`). /// List audio output devices, or set `[audio] device` if one is given.
AudioDevices, AudioDevices(AudioDevicesArgs),
/// Print a shell completion script. /// Print a shell completion script.
Completions(CompletionsArgs), Completions(CompletionsArgs),
} }
@ -251,8 +260,8 @@ pub enum CbdCommand {
Queue(QueueCmd), Queue(QueueCmd),
#[command(subcommand)] #[command(subcommand)]
Global(GlobalCmd), Global(GlobalCmd),
/// List the audio output devices (to pick `[audio] device`). /// List audio output devices, or set `[audio] device` if one is given.
AudioDevices, AudioDevices(AudioDevicesArgs),
/// Print a shell completion script. /// Print a shell completion script.
Completions(CompletionsArgs), Completions(CompletionsArgs),
} }

View File

@ -92,7 +92,7 @@ async fn run_command(
match command { match command {
CbdCommand::Guard(args) => server_cli::guard(args).await, CbdCommand::Guard(args) => server_cli::guard(args).await,
CbdCommand::Scan(args) => server_cli::scan(args).await, CbdCommand::Scan(args) => server_cli::scan(args).await,
CbdCommand::AudioDevices => server_cli::audio_devices(), CbdCommand::AudioDevices(args) => server_cli::audio_devices(args.device),
CbdCommand::Auth(args) => { CbdCommand::Auth(args) => {
let password = args let password = args
.password .password

View File

@ -102,12 +102,43 @@ pub async fn guard(args: GuardArgs) -> Result<(), Box<dyn Error>> {
Ok(()) Ok(())
} }
/// `audio-devices`: list the available audio output devices so the user can /// `audio-devices [device]`: with no argument, list the available audio output
/// choose one for `[audio] device` in `crabidy-server.toml`. Marks the device /// devices so the user can choose one for `[audio] device` in
/// the current config selects, using the same case-insensitive substring match /// `crabidy-server.toml`, marking the device the current config selects (same
/// the server applies at startup. Fixes the common Raspberry Pi case where the /// case-insensitive substring match the server applies at startup). With a
/// default device is HDMI and audio plays but is silent on the jack/DAC. /// `device` argument, write it into `[audio] device` and then list — so one
pub fn audio_devices() -> Result<(), Box<dyn Error>> { /// command both configures and confirms. Fixes the common Raspberry Pi case
/// where the default device is HDMI and audio plays but is silent on the
/// jack/DAC.
pub fn audio_devices(select: Option<String>) -> Result<(), Box<dyn Error>> {
// Setting a device needs a writable config dir; listing tolerates its
// absence.
if let Some(device) = select {
let dir = config_dir()?;
let mut settings = ServerSettings::load(&dir)?;
settings.audio.device = Some(device.clone());
settings.store(&dir)?;
println!(
"Set [audio] device = \"{device}\" in {}",
dir.join(crate::settings::SETTINGS_FILE).display()
);
// The server matches the same way; warn on a fragment that currently
// resolves to nothing so a typo is caught here, not as silent output.
let needle = device.to_lowercase();
let names = audio_player::output_device_names();
if !names.is_empty()
&& !names
.iter()
.any(|name| name.to_lowercase().contains(&needle))
{
eprintln!(
"warning: no current output device name contains \"{device}\"; the server \
will fall back to the system default until one matches"
);
}
println!("Restart the server for it to take effect.\n");
}
let dir = config_dir().ok(); let dir = config_dir().ok();
let configured = dir let configured = dir
.as_deref() .as_deref()

View File

@ -41,7 +41,7 @@ async fn run_command(
match command { match command {
ServerCommand::Guard(args) => cli::guard(args).await, ServerCommand::Guard(args) => cli::guard(args).await,
ServerCommand::Scan(args) => cli::scan(args).await, ServerCommand::Scan(args) => cli::scan(args).await,
ServerCommand::AudioDevices => cli::audio_devices(), ServerCommand::AudioDevices(args) => cli::audio_devices(args.device),
ServerCommand::Library(cmd) => { ServerCommand::Library(cmd) => {
cbd_cli::run_remote(&cli::connection(remote), RemoteCmd::Library(cmd)).await cbd_cli::run_remote(&cli::connection(remote), RemoteCmd::Library(cmd)).await
} }