Add PlayNotification support for device-local PCM files and expose it via CLI

This commit is contained in:
Tobias Gesellchen
2026-02-10 08:23:33 +01:00
parent 8094ac70bd
commit f3162b7ed9
4 changed files with 134 additions and 9 deletions
+23 -2
View File
@@ -1801,8 +1801,29 @@ func (c *Client) PlayCustom(playInfo *models.PlayInfo) error {
// PlayNotificationBeep plays a notification beep on the device
func (c *Client) PlayNotificationBeep() error {
var status models.StationResponse
return c.get("/playNotification", &status)
return c.PlayNotification("")
}
// PlayNotification plays a notification. If a non-empty local path is provided,
// it will be sent as XML body to play that specific device-local PCM file.
// When path is empty, the device's default beep is triggered.
func (c *Client) PlayNotification(path string) error {
// Empty path -> trigger default beep via GET
if strings.TrimSpace(path) == "" {
var status models.StationResponse
return c.get("/playNotification", &status)
}
// Non-empty path -> POST minimal XML payload as required by the device
payload := struct {
XMLName xml.Name `xml:"audioSource"`
PathToFile string `xml:"pathToFile,attr"`
}{
XMLName: xml.Name{Local: "audioSource"},
PathToFile: path,
}
return c.post("/playNotification", payload)
}
// Introspect retrieves introspect data for a specified music service
+69
View File
@@ -1,6 +1,7 @@
package client
import (
"io"
"net/http"
"net/http/httptest"
"net/url"
@@ -1160,3 +1161,71 @@ func TestClient_RequestToken_Error(t *testing.T) {
t.Errorf("Error should mention 'failed to request token', got: %v", err)
}
}
func TestClient_PlayNotificationBeep(t *testing.T) {
// Create mock server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/playNotification" {
t.Errorf("Expected path '/playNotification', got '%s'", r.URL.Path)
w.WriteHeader(http.StatusNotFound)
return
}
if r.Method != http.MethodGet {
t.Errorf("Expected GET method, got %s", r.Method)
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/xml")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`<?xml version="1.0" encoding="UTF-8" ?><status>success</status>`))
}))
defer server.Close()
// Create test client
client := createTestClient(server.URL)
// Test PlayNotificationBeep
err := client.PlayNotificationBeep()
if err != nil {
t.Fatalf("PlayNotificationBeep() failed: %v", err)
}
}
func TestClient_PlayNotification_Path(t *testing.T) {
testPath := "/opt/Bose/chimes/grouped.pcm"
// Create mock server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/playNotification" {
t.Errorf("Expected path '/playNotification', got '%s'", r.URL.Path)
w.WriteHeader(http.StatusNotFound)
return
}
if r.Method != http.MethodPost {
t.Errorf("Expected POST method, got %s", r.Method)
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
body, _ := io.ReadAll(r.Body)
expectedXML := `<audioSource pathToFile="` + testPath + `"></audioSource>`
if string(body) != expectedXML {
t.Errorf("Expected body '%s', got '%s'", expectedXML, string(body))
}
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
// Create test client
client := createTestClient(server.URL)
// Test PlayNotification with path
err := client.PlayNotification(testPath)
if err != nil {
t.Fatalf("PlayNotification() failed: %v", err)
}
}