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 }