mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 00:56:16 +00:00
- Add PlayInfo model for TTS and URL content playback requests - Add SpeakerResponse model for endpoint responses - Implement client methods: PlayTTS, PlayURL, PlayCustom, PlayNotificationBeep - Add comprehensive CLI commands for speaker functionality: - speaker tts: Text-to-Speech with Google TTS and language support - speaker url: Audio content playback from HTTP/HTTPS URLs - speaker beep: Simple notification beep sound - speaker help: Detailed functionality documentation - Support for volume control (0-100 or current volume) - Multi-language TTS support (EN, DE, ES, FR, IT, NL, PT, RU, ZH, JA, etc.) - Custom metadata support for NowPlaying display - Comprehensive validation and error handling - Full test suite with XML marshaling/unmarshaling tests - Complete documentation with API reference and usage examples - Compatible with ST-10 (Series III) and other supported SoundTouch devices The /speaker endpoint enables notification and audio content playback, automatically managing volume restoration and content interruption. Perfect for home automation, alerts, and custom audio notifications.
332 lines
8.5 KiB
Go
332 lines
8.5 KiB
Go
package models
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"testing"
|
|
)
|
|
|
|
func TestNewPlayInfo(t *testing.T) {
|
|
playInfo := NewPlayInfo("https://example.com/audio.mp3", "test-key", "Test Service", "Test Message", "Test Reason")
|
|
|
|
if playInfo.URL != "https://example.com/audio.mp3" {
|
|
t.Errorf("Expected URL 'https://example.com/audio.mp3', got '%s'", playInfo.URL)
|
|
}
|
|
|
|
if playInfo.AppKey != "test-key" {
|
|
t.Errorf("Expected AppKey 'test-key', got '%s'", playInfo.AppKey)
|
|
}
|
|
|
|
if playInfo.Service != "Test Service" {
|
|
t.Errorf("Expected Service 'Test Service', got '%s'", playInfo.Service)
|
|
}
|
|
|
|
if playInfo.Message != "Test Message" {
|
|
t.Errorf("Expected Message 'Test Message', got '%s'", playInfo.Message)
|
|
}
|
|
|
|
if playInfo.Reason != "Test Reason" {
|
|
t.Errorf("Expected Reason 'Test Reason', got '%s'", playInfo.Reason)
|
|
}
|
|
|
|
if playInfo.Volume != nil {
|
|
t.Errorf("Expected Volume to be nil, got %v", *playInfo.Volume)
|
|
}
|
|
}
|
|
|
|
func TestNewTTSPlayInfo(t *testing.T) {
|
|
// Test without volume
|
|
playInfo := NewTTSPlayInfo("Hello World", "test-key")
|
|
|
|
expectedURL := "http://translate.google.com/translate_tts?ie=UTF-8&tl=EN&client=tw-ob&q=Hello World"
|
|
if playInfo.URL != expectedURL {
|
|
t.Errorf("Expected URL '%s', got '%s'", expectedURL, playInfo.URL)
|
|
}
|
|
|
|
if playInfo.AppKey != "test-key" {
|
|
t.Errorf("Expected AppKey 'test-key', got '%s'", playInfo.AppKey)
|
|
}
|
|
|
|
if playInfo.Service != "TTS Notification" {
|
|
t.Errorf("Expected Service 'TTS Notification', got '%s'", playInfo.Service)
|
|
}
|
|
|
|
if playInfo.Message != "Google TTS" {
|
|
t.Errorf("Expected Message 'Google TTS', got '%s'", playInfo.Message)
|
|
}
|
|
|
|
if playInfo.Reason != "Hello World" {
|
|
t.Errorf("Expected Reason 'Hello World', got '%s'", playInfo.Reason)
|
|
}
|
|
|
|
if playInfo.Volume != nil {
|
|
t.Errorf("Expected Volume to be nil, got %v", *playInfo.Volume)
|
|
}
|
|
|
|
// Test with volume
|
|
playInfoWithVolume := NewTTSPlayInfo("Hello World", "test-key", 50)
|
|
if playInfoWithVolume.Volume == nil || *playInfoWithVolume.Volume != 50 {
|
|
t.Errorf("Expected Volume to be 50, got %v", playInfoWithVolume.Volume)
|
|
}
|
|
}
|
|
|
|
func TestNewURLPlayInfo(t *testing.T) {
|
|
// Test without volume
|
|
playInfo := NewURLPlayInfo(
|
|
"https://example.com/audio.mp3",
|
|
"test-key",
|
|
"Music Service",
|
|
"Song Title",
|
|
"Artist Name",
|
|
)
|
|
|
|
if playInfo.URL != "https://example.com/audio.mp3" {
|
|
t.Errorf("Expected URL 'https://example.com/audio.mp3', got '%s'", playInfo.URL)
|
|
}
|
|
|
|
if playInfo.Service != "Music Service" {
|
|
t.Errorf("Expected Service 'Music Service', got '%s'", playInfo.Service)
|
|
}
|
|
|
|
if playInfo.Message != "Song Title" {
|
|
t.Errorf("Expected Message 'Song Title', got '%s'", playInfo.Message)
|
|
}
|
|
|
|
if playInfo.Reason != "Artist Name" {
|
|
t.Errorf("Expected Reason 'Artist Name', got '%s'", playInfo.Reason)
|
|
}
|
|
|
|
// Test with volume
|
|
playInfoWithVolume := NewURLPlayInfo(
|
|
"https://example.com/audio.mp3",
|
|
"test-key",
|
|
"Music Service",
|
|
"Song Title",
|
|
"Artist Name",
|
|
75,
|
|
)
|
|
|
|
if playInfoWithVolume.Volume == nil || *playInfoWithVolume.Volume != 75 {
|
|
t.Errorf("Expected Volume to be 75, got %v", playInfoWithVolume.Volume)
|
|
}
|
|
}
|
|
|
|
func TestSetVolume(t *testing.T) {
|
|
playInfo := NewPlayInfo("https://example.com/audio.mp3", "test-key", "Service", "Message", "Reason")
|
|
|
|
// Set volume and check fluent interface
|
|
result := playInfo.SetVolume(60)
|
|
|
|
// Check that it returns the same instance (fluent interface)
|
|
if result != playInfo {
|
|
t.Error("SetVolume should return the same instance for fluent interface")
|
|
}
|
|
|
|
// Check that volume was set correctly
|
|
if playInfo.Volume == nil || *playInfo.Volume != 60 {
|
|
t.Errorf("Expected Volume to be 60, got %v", playInfo.Volume)
|
|
}
|
|
}
|
|
|
|
func TestPlayInfoValidate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
playInfo *PlayInfo
|
|
expectedErr error
|
|
}{
|
|
{
|
|
name: "valid PlayInfo",
|
|
playInfo: &PlayInfo{
|
|
URL: "https://example.com/audio.mp3",
|
|
AppKey: "test-key",
|
|
Service: "Test Service",
|
|
Message: "Test Message",
|
|
Reason: "Test Reason",
|
|
},
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
name: "empty URL",
|
|
playInfo: &PlayInfo{
|
|
URL: "",
|
|
AppKey: "test-key",
|
|
Service: "Test Service",
|
|
},
|
|
expectedErr: ErrInvalidURL,
|
|
},
|
|
{
|
|
name: "empty AppKey",
|
|
playInfo: &PlayInfo{
|
|
URL: "https://example.com/audio.mp3",
|
|
AppKey: "",
|
|
Service: "Test Service",
|
|
},
|
|
expectedErr: ErrInvalidAppKey,
|
|
},
|
|
{
|
|
name: "empty Service",
|
|
playInfo: &PlayInfo{
|
|
URL: "https://example.com/audio.mp3",
|
|
AppKey: "test-key",
|
|
Service: "",
|
|
},
|
|
expectedErr: ErrInvalidService,
|
|
},
|
|
{
|
|
name: "negative volume",
|
|
playInfo: &PlayInfo{
|
|
URL: "https://example.com/audio.mp3",
|
|
AppKey: "test-key",
|
|
Service: "Test Service",
|
|
Volume: intPtr(-1),
|
|
},
|
|
expectedErr: ErrInvalidVolume,
|
|
},
|
|
{
|
|
name: "volume too high",
|
|
playInfo: &PlayInfo{
|
|
URL: "https://example.com/audio.mp3",
|
|
AppKey: "test-key",
|
|
Service: "Test Service",
|
|
Volume: intPtr(101),
|
|
},
|
|
expectedErr: ErrInvalidVolume,
|
|
},
|
|
{
|
|
name: "valid volume at boundary",
|
|
playInfo: &PlayInfo{
|
|
URL: "https://example.com/audio.mp3",
|
|
AppKey: "test-key",
|
|
Service: "Test Service",
|
|
Volume: intPtr(100),
|
|
},
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
name: "valid volume at zero boundary",
|
|
playInfo: &PlayInfo{
|
|
URL: "https://example.com/audio.mp3",
|
|
AppKey: "test-key",
|
|
Service: "Test Service",
|
|
Volume: intPtr(0),
|
|
},
|
|
expectedErr: nil,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := tt.playInfo.Validate()
|
|
if (err != nil && tt.expectedErr == nil) || (err == nil && tt.expectedErr != nil) || (err != nil && tt.expectedErr != nil && err.Error() != tt.expectedErr.Error()) {
|
|
t.Errorf("Expected error %v, got %v", tt.expectedErr, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPlayInfoString(t *testing.T) {
|
|
// Test without volume
|
|
playInfo := &PlayInfo{
|
|
Service: "Test Service",
|
|
Message: "Test Message",
|
|
}
|
|
|
|
expected := "Service: Test Service, Message: Test Message, Volume: current"
|
|
result := playInfo.String()
|
|
|
|
if result != expected {
|
|
t.Errorf("Expected '%s', got '%s'", expected, result)
|
|
}
|
|
|
|
// Test with volume
|
|
playInfo.SetVolume(75)
|
|
|
|
expectedWithVolume := "Service: Test Service, Message: Test Message, Volume: K" // K is ASCII 75
|
|
resultWithVolume := playInfo.String()
|
|
|
|
if resultWithVolume != expectedWithVolume {
|
|
t.Errorf("Expected '%s', got '%s'", expectedWithVolume, resultWithVolume)
|
|
}
|
|
}
|
|
|
|
func TestPlayInfoXMLMarshaling(t *testing.T) {
|
|
// Test XML marshaling
|
|
playInfo := &PlayInfo{
|
|
URL: "https://example.com/audio.mp3",
|
|
AppKey: "test-key",
|
|
Service: "Test Service",
|
|
Message: "Test Message",
|
|
Reason: "Test Reason",
|
|
Volume: intPtr(50),
|
|
}
|
|
|
|
xmlData, err := xml.Marshal(playInfo)
|
|
if err != nil {
|
|
t.Fatalf("Failed to marshal PlayInfo to XML: %v", err)
|
|
}
|
|
|
|
expectedXML := `<play_info><url>https://example.com/audio.mp3</url><app_key>test-key</app_key><service>Test Service</service><message>Test Message</message><reason>Test Reason</reason><volume>50</volume></play_info>`
|
|
if string(xmlData) != expectedXML {
|
|
t.Errorf("Expected XML '%s', got '%s'", expectedXML, string(xmlData))
|
|
}
|
|
|
|
// Test XML unmarshaling
|
|
var unmarshaled PlayInfo
|
|
|
|
err = xml.Unmarshal(xmlData, &unmarshaled)
|
|
if err != nil {
|
|
t.Fatalf("Failed to unmarshal PlayInfo from XML: %v", err)
|
|
}
|
|
|
|
if unmarshaled.URL != playInfo.URL {
|
|
t.Errorf("Expected URL '%s', got '%s'", playInfo.URL, unmarshaled.URL)
|
|
}
|
|
|
|
if unmarshaled.AppKey != playInfo.AppKey {
|
|
t.Errorf("Expected AppKey '%s', got '%s'", playInfo.AppKey, unmarshaled.AppKey)
|
|
}
|
|
|
|
if unmarshaled.Service != playInfo.Service {
|
|
t.Errorf("Expected Service '%s', got '%s'", playInfo.Service, unmarshaled.Service)
|
|
}
|
|
|
|
if unmarshaled.Volume == nil || *unmarshaled.Volume != *playInfo.Volume {
|
|
t.Errorf("Expected Volume %v, got %v", playInfo.Volume, unmarshaled.Volume)
|
|
}
|
|
}
|
|
|
|
func TestSpeakerResponse(t *testing.T) {
|
|
// Test XML marshaling
|
|
response := &SpeakerResponse{
|
|
Value: "/speaker",
|
|
}
|
|
|
|
xmlData, err := xml.Marshal(response)
|
|
if err != nil {
|
|
t.Fatalf("Failed to marshal SpeakerResponse to XML: %v", err)
|
|
}
|
|
|
|
expectedXML := `<status>/speaker</status>`
|
|
if string(xmlData) != expectedXML {
|
|
t.Errorf("Expected XML '%s', got '%s'", expectedXML, string(xmlData))
|
|
}
|
|
|
|
// Test XML unmarshaling
|
|
xmlInput := `<?xml version="1.0" encoding="UTF-8" ?><status>/speaker</status>`
|
|
|
|
var unmarshaled SpeakerResponse
|
|
|
|
err = xml.Unmarshal([]byte(xmlInput), &unmarshaled)
|
|
if err != nil {
|
|
t.Fatalf("Failed to unmarshal SpeakerResponse from XML: %v", err)
|
|
}
|
|
|
|
if unmarshaled.Value != "/speaker" {
|
|
t.Errorf("Expected Value '/speaker', got '%s'", unmarshaled.Value)
|
|
}
|
|
}
|
|
|
|
// Helper function to create int pointer for tests
|
|
func intPtr(i int) *int {
|
|
return &i
|
|
}
|