mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 16:46:17 +00:00
feat: implement comprehensive content selection with streamUrl format support
✨ New Features: - Add SelectContentItem() method for direct ContentItem selection - Add SelectLocalInternetRadio() with full streamUrl format support - Add SelectLocalMusic() for SoundTouch App Media Server content - Add SelectStoredMusic() for UPnP/DLNA media server content 📻 streamUrl Format Support: - Full implementation of wiki specification for LOCAL_INTERNET_RADIO - Support for proxy URLs: http://contentapi.gmuth.de/station.php?name=Station&streamUrl=ActualStream - Direct stream URL support for simple internet radio - Complete ContentItem structure with metadata and artwork 🖥️ CLI Commands: - Add 'source internet-radio' command with streamUrl support - Add 'source local-music' command for local media server content - Add 'source stored-music' command for UPnP/DLNA content - Add 'source content' command for advanced generic selection - All commands include comprehensive flag support and validation 🧪 Testing: - Add 17+ comprehensive unit tests covering all scenarios - Test streamUrl format validation and parsing - Test error handling and parameter validation - Test default value assignment and ContentItem construction - All tests passing with full coverage 📚 Documentation: - Update CLI-REFERENCE.md with new command examples - Add complete content-selection example with working code - Add implementation summary document - Include API documentation for all new methods - Add usage examples for both API and CLI 🔗 References: Implements features from SoundTouch WebServices API Wiki: - https://github.com/thlucas1/homeassistantcomponent_soundtouchplus/wiki/SoundTouch-WebServices-API#select-local_internet_radio---streamurl-format - https://github.com/thlucas1/homeassistantcomponent_soundtouchplus/wiki/SoundTouch-WebServices-API#select-local_music 🎯 Benefits: - Complete API coverage for advanced content selection - Backward compatible with existing code - Flexible design with both convenience and power-user methods - Production-ready with comprehensive testing and documentation Co-authored-by: SoundTouch WebServices API Wiki <https://github.com/thlucas1/homeassistantcomponent_soundtouchplus>
This commit is contained in:
@@ -209,6 +209,218 @@ func selectAux(c *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// selectLocalInternetRadio handles selecting LOCAL_INTERNET_RADIO source
|
||||
func selectLocalInternetRadio(c *cli.Context) error {
|
||||
clientConfig := GetClientConfig(c)
|
||||
|
||||
client, err := CreateSoundTouchClient(clientConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
location := c.String("location")
|
||||
if location == "" {
|
||||
return fmt.Errorf("location is required (use --location)")
|
||||
}
|
||||
|
||||
sourceAccount := c.String("account")
|
||||
itemName := c.String("name")
|
||||
containerArt := c.String("artwork")
|
||||
|
||||
// Check LOCAL_INTERNET_RADIO availability
|
||||
checker := NewServiceAvailabilityChecker(client)
|
||||
if !checker.CheckSourceAvailable("LOCAL_INTERNET_RADIO", "select internet radio") {
|
||||
return fmt.Errorf("LOCAL_INTERNET_RADIO is not available")
|
||||
}
|
||||
|
||||
PrintDeviceHeader("Selecting internet radio stream", clientConfig.Host, clientConfig.Port)
|
||||
|
||||
if itemName != "" {
|
||||
fmt.Printf(" Station: %s\n", itemName)
|
||||
}
|
||||
fmt.Printf(" Location: %s\n", location)
|
||||
|
||||
err = client.SelectLocalInternetRadio(location, sourceAccount, itemName, containerArt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to select internet radio: %w", err)
|
||||
}
|
||||
|
||||
PrintSuccess("Internet radio stream selected")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// selectLocalMusic handles selecting LOCAL_MUSIC source
|
||||
func selectLocalMusic(c *cli.Context) error {
|
||||
clientConfig := GetClientConfig(c)
|
||||
|
||||
client, err := CreateSoundTouchClient(clientConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
location := c.String("location")
|
||||
if location == "" {
|
||||
return fmt.Errorf("location is required (use --location)")
|
||||
}
|
||||
|
||||
sourceAccount := c.String("account")
|
||||
if sourceAccount == "" {
|
||||
return fmt.Errorf("account is required for LOCAL_MUSIC (use --account)")
|
||||
}
|
||||
|
||||
itemName := c.String("name")
|
||||
containerArt := c.String("artwork")
|
||||
|
||||
// Check LOCAL_MUSIC availability
|
||||
checker := NewServiceAvailabilityChecker(client)
|
||||
if !checker.CheckSourceAvailable("LOCAL_MUSIC", "select local music") {
|
||||
return fmt.Errorf("LOCAL_MUSIC is not available")
|
||||
}
|
||||
|
||||
PrintDeviceHeader("Selecting local music content", clientConfig.Host, clientConfig.Port)
|
||||
|
||||
if itemName != "" {
|
||||
fmt.Printf(" Content: %s\n", itemName)
|
||||
}
|
||||
fmt.Printf(" Location: %s\n", location)
|
||||
fmt.Printf(" Account: %s\n", sourceAccount)
|
||||
|
||||
err = client.SelectLocalMusic(location, sourceAccount, itemName, containerArt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to select local music: %w", err)
|
||||
}
|
||||
|
||||
PrintSuccess("Local music content selected")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// selectStoredMusic handles selecting STORED_MUSIC source
|
||||
func selectStoredMusic(c *cli.Context) error {
|
||||
clientConfig := GetClientConfig(c)
|
||||
|
||||
client, err := CreateSoundTouchClient(clientConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
location := c.String("location")
|
||||
if location == "" {
|
||||
return fmt.Errorf("location is required (use --location)")
|
||||
}
|
||||
|
||||
sourceAccount := c.String("account")
|
||||
if sourceAccount == "" {
|
||||
return fmt.Errorf("account is required for STORED_MUSIC (use --account)")
|
||||
}
|
||||
|
||||
itemName := c.String("name")
|
||||
containerArt := c.String("artwork")
|
||||
|
||||
// Check STORED_MUSIC availability
|
||||
checker := NewServiceAvailabilityChecker(client)
|
||||
if !checker.CheckSourceAvailable("STORED_MUSIC", "select stored music") {
|
||||
return fmt.Errorf("STORED_MUSIC is not available")
|
||||
}
|
||||
|
||||
PrintDeviceHeader("Selecting stored music content", clientConfig.Host, clientConfig.Port)
|
||||
|
||||
if itemName != "" {
|
||||
fmt.Printf(" Content: %s\n", itemName)
|
||||
}
|
||||
fmt.Printf(" Location: %s\n", location)
|
||||
fmt.Printf(" Account: %s\n", sourceAccount)
|
||||
|
||||
err = client.SelectStoredMusic(location, sourceAccount, itemName, containerArt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to select stored music: %w", err)
|
||||
}
|
||||
|
||||
PrintSuccess("Stored music content selected")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// selectContent handles selecting content using a ContentItem directly
|
||||
func selectContent(c *cli.Context) error {
|
||||
clientConfig := GetClientConfig(c)
|
||||
|
||||
client, err := CreateSoundTouchClient(clientConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Required parameters
|
||||
source := strings.ToUpper(c.String("source"))
|
||||
if source == "" {
|
||||
return fmt.Errorf("source is required (use --source)")
|
||||
}
|
||||
|
||||
location := c.String("location")
|
||||
if location == "" {
|
||||
return fmt.Errorf("location is required (use --location)")
|
||||
}
|
||||
|
||||
// Optional parameters
|
||||
sourceAccount := c.String("account")
|
||||
itemName := c.String("name")
|
||||
containerArt := c.String("artwork")
|
||||
itemType := c.String("type")
|
||||
isPresetable := c.Bool("presetable")
|
||||
|
||||
// Create ContentItem
|
||||
contentItem := &models.ContentItem{
|
||||
Source: source,
|
||||
Type: itemType,
|
||||
Location: location,
|
||||
SourceAccount: sourceAccount,
|
||||
IsPresetable: isPresetable,
|
||||
ItemName: itemName,
|
||||
ContainerArt: containerArt,
|
||||
}
|
||||
|
||||
// Set default type if not specified
|
||||
if itemType == "" {
|
||||
switch source {
|
||||
case "SPOTIFY":
|
||||
contentItem.Type = "uri"
|
||||
case "TUNEIN", "LOCAL_INTERNET_RADIO":
|
||||
contentItem.Type = "stationurl"
|
||||
case "LOCAL_MUSIC":
|
||||
contentItem.Type = "album" // default, could be track, artist, etc.
|
||||
}
|
||||
}
|
||||
|
||||
// Set default item name if not specified
|
||||
if itemName == "" {
|
||||
contentItem.ItemName = source
|
||||
}
|
||||
|
||||
PrintDeviceHeader("Selecting content", clientConfig.Host, clientConfig.Port)
|
||||
|
||||
fmt.Printf(" Source: %s\n", source)
|
||||
fmt.Printf(" Location: %s\n", location)
|
||||
if sourceAccount != "" {
|
||||
fmt.Printf(" Account: %s\n", sourceAccount)
|
||||
}
|
||||
if itemName != "" {
|
||||
fmt.Printf(" Name: %s\n", itemName)
|
||||
}
|
||||
if itemType != "" {
|
||||
fmt.Printf(" Type: %s\n", itemType)
|
||||
}
|
||||
|
||||
err = client.SelectContentItem(contentItem)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to select content: %w", err)
|
||||
}
|
||||
|
||||
PrintSuccess("Content selected")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getServiceAvailability handles displaying service availability information
|
||||
func getServiceAvailability(c *cli.Context) error {
|
||||
clientConfig := GetClientConfig(c)
|
||||
|
||||
@@ -896,6 +896,136 @@ func main() {
|
||||
Action: selectAux,
|
||||
Before: RequireHost,
|
||||
},
|
||||
{
|
||||
Name: "internet-radio",
|
||||
Usage: "Select internet radio stream (LOCAL_INTERNET_RADIO)",
|
||||
Action: selectLocalInternetRadio,
|
||||
Before: RequireHost,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "location",
|
||||
Aliases: []string{"l"},
|
||||
Usage: "Stream location URL (direct stream or streamUrl format)",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "account",
|
||||
Aliases: []string{"a"},
|
||||
Usage: "Source account (optional)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Aliases: []string{"n"},
|
||||
Usage: "Station name",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "artwork",
|
||||
Usage: "Station artwork URL",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "local-music",
|
||||
Usage: "Select local music content (LOCAL_MUSIC)",
|
||||
Action: selectLocalMusic,
|
||||
Before: RequireHost,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "location",
|
||||
Aliases: []string{"l"},
|
||||
Usage: "Content location (e.g., album:983, track:2579)",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "account",
|
||||
Aliases: []string{"a"},
|
||||
Usage: "Source account GUID (required)",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Aliases: []string{"n"},
|
||||
Usage: "Content name",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "artwork",
|
||||
Usage: "Content artwork URL",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "stored-music",
|
||||
Usage: "Select stored music content (STORED_MUSIC)",
|
||||
Action: selectStoredMusic,
|
||||
Before: RequireHost,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "location",
|
||||
Aliases: []string{"l"},
|
||||
Usage: "Content location ID (e.g., 6_a2874b5d_4f83d999)",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "account",
|
||||
Aliases: []string{"a"},
|
||||
Usage: "Source account GUID (required)",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Aliases: []string{"n"},
|
||||
Usage: "Content name",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "artwork",
|
||||
Usage: "Content artwork URL",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "content",
|
||||
Usage: "Select content using ContentItem (advanced)",
|
||||
Action: selectContent,
|
||||
Before: RequireHost,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "source",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "Content source (SPOTIFY, TUNEIN, LOCAL_INTERNET_RADIO, etc.)",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "location",
|
||||
Aliases: []string{"l"},
|
||||
Usage: "Content location",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "account",
|
||||
Aliases: []string{"a"},
|
||||
Usage: "Source account",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Aliases: []string{"n"},
|
||||
Usage: "Content name",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "type",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "Content type (uri, stationurl, album, track, etc.)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "artwork",
|
||||
Usage: "Content artwork URL",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "presetable",
|
||||
Usage: "Mark content as presetable",
|
||||
Value: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "availability",
|
||||
Usage: "Show service availability",
|
||||
|
||||
Reference in New Issue
Block a user