diff --git a/README.md b/README.md index def4e60..34690bd 100644 --- a/README.md +++ b/README.md @@ -77,4 +77,4 @@ nix build .#proxmox-chaosknoten-nixos-template ## License This CCCHH nix-infra repository is licensed under the [MIT License](./LICENSE). -[`0001_oidc_group_and_role_mapping_custom_pipeline.patch`](patches/0001_oidc_group_and_role_mapping_custom_pipeline.patch) is licensed under the Creative Commons: CC BY-SA 4.0 license. +[`librespot_PR1528_conflicts_resolved.patch`](patches/librespot_PR1528_conflicts_resolved.patch) is a modified version of [librespot PR 1528](https://github.com/librespot-org/librespot/pull/1528) and is licensed under the [MIT license](https://github.com/librespot-org/librespot/blob/dev/LICENSE). diff --git a/flake.nix b/flake.nix index 53bf4ca..39183bf 100644 --- a/flake.nix +++ b/flake.nix @@ -40,6 +40,29 @@ proxmox-vm = ./config/proxmox-vm; prometheus-exporter = ./config/extra/prometheus-exporter.nix; }; + overlays = { + librespotFixOverlay = final: prev: { + librespot = (prev.librespot.override { withAvahi = true; }).overrideAttrs (finalAttrs: prevAttr: rec { + # Build dev branch. + name = "${prevAttr.pname}-${version}"; + version = "dev"; + src = prev.fetchFromGitHub { + owner = "librespot-org"; + repo = "librespot"; + rev = "dev"; + sha256 = "sha256-s9JpIbqXiVXMlhEuIuKio+rD1rM3kc7bAT0+8+5s35w="; + }; + cargoDeps = final.rustPlatform.fetchCargoVendor { + inherit src; + hash = "sha256-Lujz2revTAok9B0hzdl8NVQ5XMRY9ACJzoQHIkIgKMg="; + }; + # Fix librespot failing with "Unable to load audio item: Error { kind: Unavailable, error: StatusCode(500) }". + patches = (prevAttr.patches or []) ++ [ + ./patches/librespot_PR1528_conflicts_resolved.patch + ]; + }); + }; + }; nixosConfigurations = { audio-hauptraum-kueche = nixpkgs.lib.nixosSystem { inherit system specialArgs; @@ -56,6 +79,7 @@ self.nixosModules.common self.nixosModules.proxmox-vm ./config/hosts/audio-hauptraum-tafel + { nixpkgs.overlays = [ self.overlays.librespotFixOverlay ]; } ]; }; diff --git a/modules/services/audio/librespot.nix b/modules/services/audio/librespot.nix index 4c0fadb..3be5c86 100644 --- a/modules/services/audio/librespot.nix +++ b/modules/services/audio/librespot.nix @@ -19,11 +19,11 @@ in enable = true; description = "Spotify Connect Receiver Using librespot"; unitConfig = { - Requires = [ "network-online.target" "pipewire.service" ]; - After = [ "network-online.target" "pipewire.service" ]; + Requires = [ "network-online.target" "pipewire.service" "avahi-daemon.service" ]; + After = [ "network-online.target" "pipewire.service" "avahi-daemon.service" ]; }; serviceConfig = { - ExecStart = "${pkgs.librespot}/bin/librespot --name '${config.ccchh.services.audio.name}' --device-type speaker --bitrate 320 --enable-volume-normalisation --disable-audio-cache --disable-credential-cache"; + ExecStart = "${pkgs.librespot}/bin/librespot --name '${config.ccchh.services.audio.name}' --device-type speaker --bitrate 320 --enable-volume-normalisation --disable-audio-cache --disable-credential-cache --zeroconf-backend avahi"; User = "librespot"; Group = "librespot"; }; diff --git a/patches/librespot_PR1528_conflicts_resolved.patch b/patches/librespot_PR1528_conflicts_resolved.patch new file mode 100644 index 0000000..f97a38a --- /dev/null +++ b/patches/librespot_PR1528_conflicts_resolved.patch @@ -0,0 +1,223 @@ +From c4c968e594edcfce231682db5563f7186da7c6f0 Mon Sep 17 00:00:00 2001 +From: Timon de Groot +Date: Thu, 7 Aug 2025 12:22:56 +0200 +Subject: [PATCH 1/5] spclient: Specify base url for metadata requests + +This fixes #1527 +--- + core/src/spclient.rs | 15 +++++++++++++-- + 1 file changed, 13 insertions(+), 2 deletions(-) + +diff --git a/core/src/spclient.rs b/core/src/spclient.rs +index 87a6098..56c4287 100644 +--- a/core/src/spclient.rs ++++ b/core/src/spclient.rs +@@ -55,6 +55,7 @@ const CONNECTION_ID: HeaderName = HeaderName::from_static("x-spotify-connection- + const NO_METRICS_AND_SALT: RequestOptions = RequestOptions { + metrics: false, + salt: false, ++ base_url: None, + }; + + #[derive(Debug, Error)] +@@ -86,6 +87,7 @@ impl Default for RequestStrategy { + pub struct RequestOptions { + metrics: bool, + salt: bool, ++ base_url: Option, + } + + impl Default for RequestOptions { +@@ -93,6 +95,7 @@ impl Default for RequestOptions { + Self { + metrics: true, + salt: true, ++ base_url: None, + } + } + } +@@ -449,7 +452,10 @@ impl SpClient { + + // Reconnection logic: retrieve the endpoint every iteration, so we can try + // another access point when we are experiencing network issues (see below). +- let mut url = self.base_url().await?; ++ let mut url = match &options.base_url { ++ Some(base_url) => base_url.clone(), ++ None => self.base_url().await?, ++ }; + url.push_str(endpoint); + + // Add metrics. There is also an optional `partner` key with a value like +@@ -566,7 +572,12 @@ impl SpClient { + + pub async fn get_metadata(&self, scope: &str, id: &SpotifyId) -> SpClientResult { + let endpoint = format!("/metadata/4/{}/{}", scope, id.to_base16()?); +- self.request(&Method::GET, &endpoint, None, None).await ++ let options = RequestOptions { ++ base_url: Some(String::from("https://spclient.wg.spotify.com")), ++ ..Default::default() ++ }; ++ self.request_with_options(&Method::GET, &endpoint, None, None, &options) ++ .await + } + + pub async fn get_track_metadata(&self, track_id: &SpotifyId) -> SpClientResult { +-- +2.49.0 + + +From 2b72f3fbdf6519321feeaaecc1ea6e1bb042074e Mon Sep 17 00:00:00 2001 +From: Timon de Groot +Date: Thu, 7 Aug 2025 13:51:55 +0200 +Subject: [PATCH 2/5] spclient: Change RequestOptions to &str + +This will allocate less strings and makes it possible to have const +request option values. + +Also document why the metadata base url workaround is needed. +--- + core/src/spclient.rs | 9 ++++++--- + 1 file changed, 6 insertions(+), 3 deletions(-) + +diff --git a/core/src/spclient.rs b/core/src/spclient.rs +index 56c4287..11bcef4 100644 +--- a/core/src/spclient.rs ++++ b/core/src/spclient.rs +@@ -87,7 +87,7 @@ impl Default for RequestStrategy { + pub struct RequestOptions { + metrics: bool, + salt: bool, +- base_url: Option, ++ base_url: Option<&'static str>, + } + + impl Default for RequestOptions { +@@ -453,7 +453,7 @@ impl SpClient { + // Reconnection logic: retrieve the endpoint every iteration, so we can try + // another access point when we are experiencing network issues (see below). + let mut url = match &options.base_url { +- Some(base_url) => base_url.clone(), ++ Some(base_url) => base_url.to_owned().to_string(), + None => self.base_url().await?, + }; + url.push_str(endpoint); +@@ -572,8 +572,11 @@ impl SpClient { + + pub async fn get_metadata(&self, scope: &str, id: &SpotifyId) -> SpClientResult { + let endpoint = format!("/metadata/4/{}/{}", scope, id.to_base16()?); ++ // For unknown reasons, metadata requests must now be sent through spclient.wg.spotify.com. ++ // Otherwise, the API will respond with 500 Internal Server Error responses. ++ // Context: https://github.com/librespot-org/librespot/issues/1527 + let options = RequestOptions { +- base_url: Some(String::from("https://spclient.wg.spotify.com")), ++ base_url: Some("https://spclient.wg.spotify.com"), + ..Default::default() + }; + self.request_with_options(&Method::GET, &endpoint, None, None, &options) +-- +2.49.0 + + +From 73ed5c50849bb660834cd0d7aaa7110c01397055 Mon Sep 17 00:00:00 2001 +From: Timon de Groot +Date: Sat, 9 Aug 2025 09:28:51 +0200 +Subject: [PATCH 3/5] spclient: Make const request options for get_metadata + +--- + core/src/spclient.rs | 20 ++++++++++++++------ + 1 file changed, 14 insertions(+), 6 deletions(-) + +diff --git a/core/src/spclient.rs b/core/src/spclient.rs +index 11bcef4..cbcf092 100644 +--- a/core/src/spclient.rs ++++ b/core/src/spclient.rs +@@ -58,6 +58,12 @@ const NO_METRICS_AND_SALT: RequestOptions = RequestOptions { + base_url: None, + }; + ++const SPCLIENT_FALLBACK_ENDPOINT: RequestOptions = RequestOptions { ++ metrics: true, ++ salt: true, ++ base_url: Some("https://spclient.wg.spotify.com"), ++}; ++ + #[derive(Debug, Error)] + pub enum SpClientError { + #[error("missing attribute {0}")] +@@ -575,12 +581,14 @@ impl SpClient { + // For unknown reasons, metadata requests must now be sent through spclient.wg.spotify.com. + // Otherwise, the API will respond with 500 Internal Server Error responses. + // Context: https://github.com/librespot-org/librespot/issues/1527 +- let options = RequestOptions { +- base_url: Some("https://spclient.wg.spotify.com"), +- ..Default::default() +- }; +- self.request_with_options(&Method::GET, &endpoint, None, None, &options) +- .await ++ self.request_with_options( ++ &Method::GET, ++ &endpoint, ++ None, ++ None, ++ &SPCLIENT_FALLBACK_ENDPOINT, ++ ) ++ .await + } + + pub async fn get_track_metadata(&self, track_id: &SpotifyId) -> SpClientResult { +-- +2.49.0 + + +From 6adca21fdf64bd8026a2d6df04c42dd2b1239358 Mon Sep 17 00:00:00 2001 +From: Timon de Groot +Date: Sat, 9 Aug 2025 09:40:20 +0200 +Subject: [PATCH 4/5] spclient: Simplify base url init + +--- + core/src/spclient.rs | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/core/src/spclient.rs b/core/src/spclient.rs +index cbcf092..272975d 100644 +--- a/core/src/spclient.rs ++++ b/core/src/spclient.rs +@@ -458,8 +458,8 @@ impl SpClient { + + // Reconnection logic: retrieve the endpoint every iteration, so we can try + // another access point when we are experiencing network issues (see below). +- let mut url = match &options.base_url { +- Some(base_url) => base_url.to_owned().to_string(), ++ let mut url = match options.base_url { ++ Some(base_url) => base_url.to_string(), + None => self.base_url().await?, + }; + url.push_str(endpoint); +-- +2.49.0 + + +From 0b5b1eb6c73a9291057b3856939f416113fdd8bb Mon Sep 17 00:00:00 2001 +From: Timon de Groot +Date: Sat, 9 Aug 2025 10:14:02 +0200 +Subject: [PATCH 5/5] Update CHANGELOG.md + +--- + CHANGELOG.md | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/CHANGELOG.md b/CHANGELOG.md +index 560de2b..b62e9f8 100644 +--- a/CHANGELOG.md ++++ b/CHANGELOG.md +@@ -51,6 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 + - [connect] Correctly apply playing/paused state when transferring playback + - [player] Saturate invalid seek positions to track duration + - [audio] Fall back to other URLs in case of a failure when downloading from CDN ++- [core] Metadata requests failing with 500 Internal Server Error + + ### Deprecated + +-- +2.49.0 +