mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-10 20:56:14 +00:00
The help text still listed only tts/url/beep/notify. Add the UPnP AVTransport option (no app key, no DNS; http:// only, replaces source). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
257 lines
7.7 KiB
Go
257 lines
7.7 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/gesellix/bose-soundtouch/pkg/models"
|
|
"github.com/urfave/cli/v2"
|
|
)
|
|
|
|
// playTTS plays a Text-To-Speech message on the speaker
|
|
func playTTS(c *cli.Context) error {
|
|
clientConfig := GetClientConfig(c)
|
|
text := c.String("text")
|
|
appKey := c.String("app-key")
|
|
volume := c.Int("volume")
|
|
language := c.String("language")
|
|
|
|
if text == "" {
|
|
PrintError("Text message is required")
|
|
return fmt.Errorf("text message cannot be empty")
|
|
}
|
|
|
|
if appKey == "" {
|
|
PrintError("App key is required")
|
|
return fmt.Errorf("app key cannot be empty")
|
|
}
|
|
|
|
PrintDeviceHeader(fmt.Sprintf("Playing TTS message: \"%s\"", text), clientConfig.Host, clientConfig.Port)
|
|
|
|
client, err := CreateSoundTouchClient(clientConfig)
|
|
if err != nil {
|
|
PrintError(fmt.Sprintf("Failed to create client: %v", err))
|
|
return err
|
|
}
|
|
|
|
// Create PlayInfo for TTS
|
|
var playInfo *models.PlayInfo
|
|
if volume > 0 {
|
|
playInfo = models.NewTTSPlayInfo(text, appKey, language, volume)
|
|
} else {
|
|
playInfo = models.NewTTSPlayInfo(text, appKey, language)
|
|
}
|
|
|
|
err = client.PlayCustom(playInfo)
|
|
if err != nil {
|
|
PrintError(fmt.Sprintf("Failed to play TTS message: %v", err))
|
|
return err
|
|
}
|
|
|
|
fmt.Printf("✅ TTS message sent successfully\n")
|
|
|
|
if volume > 0 {
|
|
fmt.Printf(" Volume: %d\n", volume)
|
|
} else {
|
|
fmt.Printf(" Volume: current level\n")
|
|
}
|
|
|
|
fmt.Printf(" Language: %s\n", strings.ToUpper(language))
|
|
fmt.Printf(" Message: \"%s\"\n", text)
|
|
|
|
return nil
|
|
}
|
|
|
|
// playURL plays audio content from a URL on the speaker
|
|
func playURL(c *cli.Context) error {
|
|
clientConfig := GetClientConfig(c)
|
|
urlStr := c.String("url")
|
|
appKey := c.String("app-key")
|
|
service := c.String("service")
|
|
message := c.String("message")
|
|
reason := c.String("reason")
|
|
volume := c.Int("volume")
|
|
|
|
if urlStr == "" {
|
|
PrintError("URL is required")
|
|
return fmt.Errorf("URL cannot be empty")
|
|
}
|
|
|
|
if appKey == "" {
|
|
PrintError("App key is required")
|
|
return fmt.Errorf("app key cannot be empty")
|
|
}
|
|
|
|
// Set defaults if not provided
|
|
if service == "" {
|
|
service = "URL Playback"
|
|
}
|
|
|
|
if message == "" {
|
|
message = "Audio Content"
|
|
}
|
|
|
|
if reason == "" {
|
|
// Extract filename or use URL as reason
|
|
if idx := strings.LastIndex(urlStr, "/"); idx != -1 && idx < len(urlStr)-1 {
|
|
reason = urlStr[idx+1:]
|
|
} else {
|
|
reason = urlStr
|
|
}
|
|
}
|
|
|
|
PrintDeviceHeader(fmt.Sprintf("Playing URL: %s", urlStr), clientConfig.Host, clientConfig.Port)
|
|
|
|
client, err := CreateSoundTouchClient(clientConfig)
|
|
if err != nil {
|
|
PrintError(fmt.Sprintf("Failed to create client: %v", err))
|
|
return err
|
|
}
|
|
|
|
// Create PlayInfo for URL content
|
|
var playInfo *models.PlayInfo
|
|
if volume > 0 {
|
|
playInfo = models.NewURLPlayInfo(urlStr, appKey, service, message, reason, volume)
|
|
} else {
|
|
playInfo = models.NewURLPlayInfo(urlStr, appKey, service, message, reason)
|
|
}
|
|
|
|
err = client.PlayCustom(playInfo)
|
|
if err != nil {
|
|
PrintError(fmt.Sprintf("Failed to play URL content: %v", err))
|
|
return err
|
|
}
|
|
|
|
fmt.Printf("✅ URL playback started successfully\n")
|
|
fmt.Printf(" URL: %s\n", urlStr)
|
|
fmt.Printf(" Service: %s\n", service)
|
|
fmt.Printf(" Message: %s\n", message)
|
|
|
|
if volume > 0 {
|
|
fmt.Printf(" Volume: %d\n", volume)
|
|
} else {
|
|
fmt.Printf(" Volume: current level\n")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// playURLUPnP plays audio from a URL via the speaker's UPnP AVTransport service.
|
|
// Unlike `speaker url` (the /speaker play_info path), it needs no app-key and no
|
|
// DNS interception, so it works on a plain LAN. It switches the speaker to the
|
|
// UPNP source and replaces the current playback (no duck-and-resume), and the
|
|
// speaker itself must be able to reach the URL.
|
|
func playURLUPnP(c *cli.Context) error {
|
|
clientConfig := GetClientConfig(c)
|
|
urlStr := c.String("url")
|
|
|
|
if urlStr == "" {
|
|
PrintError("URL is required")
|
|
return fmt.Errorf("URL cannot be empty")
|
|
}
|
|
|
|
PrintDeviceHeader(fmt.Sprintf("Playing URL via UPnP: %s", urlStr), clientConfig.Host, clientConfig.Port)
|
|
|
|
client, err := CreateSoundTouchClient(clientConfig)
|
|
if err != nil {
|
|
PrintError(fmt.Sprintf("Failed to create client: %v", err))
|
|
return err
|
|
}
|
|
|
|
if err := client.PlayURLViaUPnP(urlStr); err != nil {
|
|
PrintError(fmt.Sprintf("Failed to play URL via UPnP: %v", err))
|
|
return err
|
|
}
|
|
|
|
fmt.Printf("✅ URL playback started via UPnP\n")
|
|
fmt.Printf(" URL: %s\n", urlStr)
|
|
fmt.Printf(" Note: replaces the current source (UPNP); no app-key or DNS needed\n")
|
|
|
|
return nil
|
|
}
|
|
|
|
// playNotification plays a notification sound or a local file on the speaker
|
|
func playNotification(c *cli.Context) error {
|
|
clientConfig := GetClientConfig(c)
|
|
path := c.String("path")
|
|
|
|
if path != "" {
|
|
PrintDeviceHeader(fmt.Sprintf("Playing notification file: %s", path), clientConfig.Host, clientConfig.Port)
|
|
} else {
|
|
PrintDeviceHeader("Playing notification beep", clientConfig.Host, clientConfig.Port)
|
|
}
|
|
|
|
client, err := CreateSoundTouchClient(clientConfig)
|
|
if err != nil {
|
|
PrintError(fmt.Sprintf("Failed to create client: %v", err))
|
|
return err
|
|
}
|
|
|
|
err = client.PlayNotification(path)
|
|
if err != nil {
|
|
if path != "" {
|
|
PrintError(fmt.Sprintf("Failed to play notification file: %v", err))
|
|
} else {
|
|
PrintError(fmt.Sprintf("Failed to play notification beep: %v", err))
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
if path != "" {
|
|
fmt.Printf("✅ Notification file sent successfully: %s\n", path)
|
|
} else {
|
|
fmt.Printf("✅ Notification beep played successfully\n")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// playNotificationBeep plays a notification beep on the speaker (uses existing endpoint)
|
|
func playNotificationBeep(c *cli.Context) error {
|
|
return playNotification(c)
|
|
}
|
|
|
|
// showSpeakerHelp displays help information about speaker functionality
|
|
func showSpeakerHelp(_ *cli.Context) error {
|
|
fmt.Println("SoundTouch Speaker Playback Commands")
|
|
fmt.Println("=====================================")
|
|
fmt.Println()
|
|
fmt.Println("The /speaker endpoint supports playing notifications and URL content:")
|
|
fmt.Println()
|
|
fmt.Println("• Text-to-Speech (TTS) Messages:")
|
|
fmt.Println(" Play spoken messages using Google TTS")
|
|
fmt.Println(" Example: soundtouch-cli speaker tts --text \"Hello World\" --app-key YOUR_KEY")
|
|
fmt.Println()
|
|
fmt.Println("• URL Content Playback:")
|
|
fmt.Println(" Play audio files from HTTP/HTTPS URLs")
|
|
fmt.Println(" Example: soundtouch-cli speaker url --url \"https://example.com/audio.mp3\" --app-key YOUR_KEY")
|
|
fmt.Println()
|
|
fmt.Println("• URL via UPnP/AVTransport (no app key, no DNS):")
|
|
fmt.Println(" Play an http:// audio URL directly via the speaker's UPnP renderer.")
|
|
fmt.Println(" Replaces the current source; http:// only (https is rejected).")
|
|
fmt.Println(" Example: soundtouch-cli speaker url-upnp --url \"http://192.0.2.10/audio.mp3\"")
|
|
fmt.Println()
|
|
fmt.Println("• Notification Beep:")
|
|
fmt.Println(" Play a simple notification sound")
|
|
fmt.Println(" Example: soundtouch-cli speaker beep")
|
|
fmt.Println()
|
|
fmt.Println("• Custom Notification:")
|
|
fmt.Println(" Play a device-local PCM file as notification")
|
|
fmt.Println(" Example: soundtouch-cli speaker notify --path \"/opt/Bose/chimes/grouped.pcm\"")
|
|
fmt.Println()
|
|
fmt.Println("Notes:")
|
|
fmt.Println("• Only ST-10 (Series III) speakers support the /speaker endpoint")
|
|
fmt.Println("• ST-300 and other models may not support this functionality")
|
|
fmt.Println("• You need to provide your own app_key for TTS and URL playback")
|
|
fmt.Println("• Currently playing content is paused during playback and resumed after")
|
|
fmt.Println("• If device is a zone master, content plays on all zone members")
|
|
fmt.Println("• Volume is automatically restored after playback completes")
|
|
fmt.Println()
|
|
fmt.Println("Supported Languages for TTS:")
|
|
fmt.Println("EN (English), DE (German), ES (Spanish), FR (French), IT (Italian),")
|
|
fmt.Println("NL (Dutch), PT (Portuguese), RU (Russian), ZH (Chinese), JA (Japanese)")
|
|
|
|
return nil
|
|
}
|