Skip to content
Open
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
22 changes: 22 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Rust

on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]

env:
CARGO_TERM_COLOR: always

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ thiserror = "2.0"
time = { version = "0.3.47", default-features = false, features = ["formatting"] }
clap = { version = "4.5.23", features = ["derive"] }
serde_ignored = "0.1.10"
# Pinned: 0.9 renamed MainLoop/Context/Core to *Rc variants, which
# pipewire_backend.rs/pipewire_mixer.rs don't use yet.
pipewire = { version = "=0.8.0", optional = true }
ringbuf = { version = "0.4", optional = true }

[target.'cfg(unix)'.dependencies]
daemonize = "0.5"
Expand All @@ -62,6 +66,7 @@ env_logger = "0.11"
alsa_backend = ["librespot-playback/alsa-backend", "dep:alsa"]
dbus_mpris = ["dep:dbus", "dep:dbus-tokio", "dep:dbus-crossroads"]
default = ["alsa_backend", "pulseaudio_backend", "dbus_mpris"]
pipewire_backend = ["dep:pipewire", "dep:ringbuf"]
portaudio_backend = ["librespot-playback/portaudio-backend"]
pulseaudio_backend = ["librespot-playback/pulseaudio-backend"]
rodio_backend = ["librespot-playback/rodio-backend"]
Expand Down
3 changes: 2 additions & 1 deletion docs/src/configuration/audio.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Instead of using the default device, which can sometimes be different to what yo

- For ALSA, you can use `aplay -L` to get a list of possible devices.
- For PulseAudio, run `pactl list short sinks` to get a list of possible names.
- For PipeWire, run `wpctl status` to get a list of sink names, or leave unset to use the current default sink.

## Bitrate

Expand All @@ -29,7 +30,7 @@ To reduce bandwidth usage or increase quality, you can play with the bitrate.

In most cases, leaving this at the default (`softvol`) should be fine.

If you want your `spotifyd` volume to be synchronized with an output device's hardware volume, you can set this to `alsa` or `alsa_linear`. In both cases, you might also want to set the `mixer` device to set which device's volume should be changed.
If you want your `spotifyd` volume to be synchronized with an output device's hardware volume, you can set this to `alsa` or `alsa_linear` (with the `mixer`/`control` settings), or, when using the PipeWire backend, `pipewire` or `pipewire_linear` (with the `pipewire_mixer_mode`/`pipewire_mixer_device` settings).

If you want to prevent the user to be able to adjust the volume, set this instead to `none`.

Expand Down
50 changes: 46 additions & 4 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ pub enum VolumeController {
Alsa,
#[cfg(feature = "alsa_backend")]
AlsaLinear,
#[cfg(feature = "pipewire_backend")]
Pipewire,
#[cfg(feature = "pipewire_backend")]
PipewireLinear,
#[serde(rename = "softvol")]
SoftVolume,
None,
Expand Down Expand Up @@ -168,7 +172,12 @@ impl From<AudioFormat> for LSAudioFormat {
}

fn possible_backends() -> Vec<&'static str> {
audio_backend::BACKENDS.iter().map(|b| b.0).collect()
let mut backends: Vec<&'static str> = audio_backend::BACKENDS.iter().map(|b| b.0).collect();
// Not in librespot's backend table; add explicitly or CLI/config
// validation rejects "pipewire".
#[cfg(feature = "pipewire_backend")]
backends.push("pipewire");
backends
}

fn deserialize_backend<'de, D>(de: D) -> Result<Option<String>, D::Error>
Expand Down Expand Up @@ -363,6 +372,11 @@ pub struct SharedConfigValues {
#[serde(flatten)]
alsa_config: AlsaConfig,

#[cfg(feature = "pipewire_backend")]
#[command(flatten)]
#[serde(flatten)]
pipewire_config: PipewireConfig,

#[cfg(feature = "dbus_mpris")]
#[command(flatten)]
#[serde(flatten)]
Expand Down Expand Up @@ -391,15 +405,28 @@ pub struct MprisConfig {
#[cfg(feature = "alsa_backend")]
#[derive(Debug, Default, Clone, Deserialize, Args, PartialEq, Eq)]
pub struct AlsaConfig {
/// The control device
/// The ALSA control device
#[arg(long)]
pub(crate) control: Option<String>,

/// The mixer to use
/// The ALSA mixer to use
#[arg(long)]
pub(crate) mixer: Option<String>,
}

#[cfg(feature = "pipewire_backend")]
#[derive(Debug, Default, Clone, Deserialize, Args, PartialEq, Eq)]
pub struct PipewireConfig {
/// The PipeWire mixer "stream" (default) or "sink".
#[arg(long)]
pub(crate) pipewire_mixer_mode: Option<String>,

/// For PipeWire "sink" mixer. The name of the PipeWire sink node
/// Leave unset for `default.audio.sink`
#[arg(long)]
pub(crate) pipewire_mixer_device: Option<String>,
}

#[derive(Debug, Default, Deserialize)]
pub struct FileConfig {
global: Option<SharedConfigValues>,
Expand All @@ -421,7 +448,11 @@ impl FileConfig {
#[derive(Copy, Clone)]
enum KnownConfigProblem {
#[cfg_attr(
all(feature = "alsa_backend", feature = "dbus_mpris"),
all(
feature = "alsa_backend",
feature = "pipewire_backend",
feature = "dbus_mpris"
),
expect(dead_code)
)]
MissingFeature(&'static str),
Expand All @@ -435,6 +466,11 @@ fn get_known_config_problem(path: &serde_ignored::Path<'_>) -> Option<KnownConfi
KnownConfigProblem::MissingFeature("alsa_backend"),
&["control", "mixer"],
),
#[cfg(not(feature = "pipewire_backend"))]
(
KnownConfigProblem::MissingFeature("pipewire_backend"),
&["pipewire_mixer_mode", "pipewire_mixer_device"],
),
#[cfg(not(feature = "dbus_mpris"))]
(
KnownConfigProblem::MissingFeature("dbus_mpris"),
Expand Down Expand Up @@ -594,6 +630,8 @@ impl SharedConfigValues {
merge!(self.mpris_config; and other.mpris_config => {use_mpris, dbus_type});
#[cfg(feature = "alsa_backend")]
merge!(self.alsa_config; and other.alsa_config => {mixer, control});
#[cfg(feature = "pipewire_backend")]
merge!(self.pipewire_config; and other.pipewire_config => {pipewire_mixer_mode, pipewire_mixer_device});
}
}

Expand Down Expand Up @@ -639,6 +677,8 @@ pub(crate) struct SpotifydConfig {
pub(crate) mpris: MprisConfig,
#[cfg(feature = "alsa_backend")]
pub(crate) alsa_config: AlsaConfig,
#[cfg(feature = "pipewire_backend")]
pub(crate) pipewire_config: PipewireConfig,
}

pub(crate) fn get_internal_config(config: CliConfig) -> SpotifydConfig {
Expand Down Expand Up @@ -764,6 +804,8 @@ pub(crate) fn get_internal_config(config: CliConfig) -> SpotifydConfig {
mpris: config.shared_config.mpris_config,
#[cfg(feature = "alsa_backend")]
alsa_config: config.shared_config.alsa_config,
#[cfg(feature = "pipewire_backend")]
pipewire_config: config.shared_config.pipewire_config,
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ mod error;
mod main_loop;
mod no_mixer;
mod oauth;
#[cfg(feature = "pipewire_backend")]
mod pipewire_backend;
#[cfg(feature = "pipewire_backend")]
mod pipewire_mixer;
mod process;
mod setup;
mod utils;
Expand Down
Loading