mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-09 12:16:15 +00:00
apply/fix golangci-lint findings
This commit is contained in:
+68
-60
@@ -14,6 +14,72 @@ import (
|
||||
"github.com/hashicorp/mdns"
|
||||
)
|
||||
|
||||
func displayResults(services []ServiceInfo) {
|
||||
if len(services) == 0 {
|
||||
fmt.Println("No services found.")
|
||||
fmt.Println()
|
||||
fmt.Println("This could mean:")
|
||||
fmt.Println("- No mDNS services on network")
|
||||
fmt.Println("- Network blocks multicast traffic")
|
||||
fmt.Println("- Firewall blocks mDNS port 5353")
|
||||
fmt.Println("- Try different service types or increase timeout")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Group services by type for better display
|
||||
serviceGroups := make(map[string][]ServiceInfo)
|
||||
for _, s := range services {
|
||||
serviceGroups[s.ServiceType] = append(serviceGroups[s.ServiceType], s)
|
||||
}
|
||||
|
||||
// Display grouped services
|
||||
for serviceType, serviceList := range serviceGroups {
|
||||
fmt.Printf("Service Type: %s\n", serviceType)
|
||||
fmt.Printf(" Found %d instance(s):\n", len(serviceList))
|
||||
|
||||
for i, s := range serviceList {
|
||||
fmt.Printf(" %d. %s\n", i+1, s.Name)
|
||||
|
||||
if s.Host != "" {
|
||||
fmt.Printf(" Host: %s\n", s.Host)
|
||||
}
|
||||
|
||||
if s.IPv4 != "" {
|
||||
fmt.Printf(" IPv4: %s\n", s.IPv4)
|
||||
}
|
||||
|
||||
if s.IPv6 != "" {
|
||||
fmt.Printf(" IPv6: %s\n", s.IPv6)
|
||||
}
|
||||
|
||||
if s.Port > 0 {
|
||||
fmt.Printf(" Port: %d\n", s.Port)
|
||||
}
|
||||
|
||||
if len(s.TxtRecords) > 0 {
|
||||
fmt.Printf(" TXT Records: %v\n", s.TxtRecords)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
func showSuggestions(service string) {
|
||||
if service == "_services._dns-sd._udp" {
|
||||
fmt.Println("Common services to look for SoundTouch devices:")
|
||||
fmt.Println("- _soundtouch._tcp.local.")
|
||||
fmt.Println("- _http._tcp.local.")
|
||||
fmt.Println("- _upnp._tcp.local.")
|
||||
fmt.Println("- _device-info._tcp.local.")
|
||||
fmt.Println()
|
||||
fmt.Println("Try scanning specific services:")
|
||||
fmt.Println(" ./mdns-scanner -service _soundtouch._tcp -v")
|
||||
fmt.Println(" ./mdns-scanner -service _http._tcp -v")
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
verbose := flag.Bool("verbose", false, "Enable verbose logging")
|
||||
timeout := flag.Duration("timeout", 10*time.Second, "Discovery timeout")
|
||||
@@ -107,68 +173,10 @@ done:
|
||||
})
|
||||
|
||||
// Display results
|
||||
if len(services) == 0 {
|
||||
fmt.Println("No services found.")
|
||||
fmt.Println()
|
||||
fmt.Println("This could mean:")
|
||||
fmt.Println("- No mDNS services on network")
|
||||
fmt.Println("- Network blocks multicast traffic")
|
||||
fmt.Println("- Firewall blocks mDNS port 5353")
|
||||
fmt.Println("- Try different service types or increase timeout")
|
||||
} else {
|
||||
// Group services by type for better display
|
||||
serviceGroups := make(map[string][]ServiceInfo)
|
||||
|
||||
for _, service := range services {
|
||||
serviceType := service.ServiceType
|
||||
serviceGroups[serviceType] = append(serviceGroups[serviceType], service)
|
||||
}
|
||||
|
||||
// Display grouped services
|
||||
for serviceType, serviceList := range serviceGroups {
|
||||
fmt.Printf("Service Type: %s\n", serviceType)
|
||||
fmt.Printf(" Found %d instance(s):\n", len(serviceList))
|
||||
|
||||
for i, service := range serviceList {
|
||||
fmt.Printf(" %d. %s\n", i+1, service.Name)
|
||||
|
||||
if service.Host != "" {
|
||||
fmt.Printf(" Host: %s\n", service.Host)
|
||||
}
|
||||
|
||||
if service.IPv4 != "" {
|
||||
fmt.Printf(" IPv4: %s\n", service.IPv4)
|
||||
}
|
||||
|
||||
if service.IPv6 != "" {
|
||||
fmt.Printf(" IPv6: %s\n", service.IPv6)
|
||||
}
|
||||
|
||||
if service.Port > 0 {
|
||||
fmt.Printf(" Port: %d\n", service.Port)
|
||||
}
|
||||
|
||||
if len(service.TxtRecords) > 0 {
|
||||
fmt.Printf(" TXT Records: %v\n", service.TxtRecords)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
displayResults(services)
|
||||
|
||||
// Show suggestions for common SoundTouch-related services
|
||||
if *service == "_services._dns-sd._udp" {
|
||||
fmt.Println("Common services to look for SoundTouch devices:")
|
||||
fmt.Println("- _soundtouch._tcp.local.")
|
||||
fmt.Println("- _http._tcp.local.")
|
||||
fmt.Println("- _upnp._tcp.local.")
|
||||
fmt.Println("- _device-info._tcp.local.")
|
||||
fmt.Println()
|
||||
fmt.Println("Try scanning specific services:")
|
||||
fmt.Println(" ./mdns-scanner -service _soundtouch._tcp -v")
|
||||
fmt.Println(" ./mdns-scanner -service _http._tcp -v")
|
||||
}
|
||||
showSuggestions(*service)
|
||||
}
|
||||
|
||||
type ServiceInfo struct {
|
||||
|
||||
@@ -3,9 +3,47 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gesellix/bose-soundtouch/pkg/models"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func printNetworkInterface(i int, iface *models.NetworkInterface) {
|
||||
fmt.Printf("\n Interface %d:\n", i+1)
|
||||
fmt.Printf(" Type: %s\n", iface.GetType())
|
||||
|
||||
if iface.GetName() != "" {
|
||||
fmt.Printf(" Name: %s\n", iface.GetName())
|
||||
}
|
||||
|
||||
if iface.GetIPAddress() != "" {
|
||||
fmt.Printf(" IP Address: %s\n", iface.GetIPAddress())
|
||||
}
|
||||
|
||||
if iface.GetMacAddress() != "" {
|
||||
fmt.Printf(" MAC Address: %s\n", iface.GetMacAddress())
|
||||
}
|
||||
|
||||
fmt.Printf(" State: %s\n", iface.GetStateDescription())
|
||||
|
||||
if iface.IsWiFi() {
|
||||
if iface.GetSSID() != "" {
|
||||
fmt.Printf(" SSID: %s\n", iface.GetSSID())
|
||||
}
|
||||
|
||||
if iface.GetSignal() != "" {
|
||||
fmt.Printf(" Signal: %s (%d%%)\n", iface.GetSignalDescription(), iface.GetSignalQuality())
|
||||
}
|
||||
|
||||
if iface.GetFrequencyKHz() > 0 {
|
||||
fmt.Printf(" Frequency: %s (%s)\n", iface.FormatFrequency(), iface.GetFrequencyBand())
|
||||
}
|
||||
|
||||
if iface.GetMode() != "" {
|
||||
fmt.Printf(" Mode: %s\n", iface.GetModeDescription())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getNetworkInfo retrieves network information from the device
|
||||
func getNetworkInfo(c *cli.Context) error {
|
||||
clientConfig := GetClientConfig(c)
|
||||
@@ -38,41 +76,7 @@ func getNetworkInfo(c *cli.Context) error {
|
||||
fmt.Printf(" Interfaces (%d):\n", len(interfaces))
|
||||
|
||||
for i := range interfaces {
|
||||
iface := &interfaces[i]
|
||||
fmt.Printf("\n Interface %d:\n", i+1)
|
||||
fmt.Printf(" Type: %s\n", iface.GetType())
|
||||
|
||||
if iface.GetName() != "" {
|
||||
fmt.Printf(" Name: %s\n", iface.GetName())
|
||||
}
|
||||
|
||||
if iface.GetIPAddress() != "" {
|
||||
fmt.Printf(" IP Address: %s\n", iface.GetIPAddress())
|
||||
}
|
||||
|
||||
if iface.GetMacAddress() != "" {
|
||||
fmt.Printf(" MAC Address: %s\n", iface.GetMacAddress())
|
||||
}
|
||||
|
||||
fmt.Printf(" State: %s\n", iface.GetStateDescription())
|
||||
|
||||
if iface.IsWiFi() {
|
||||
if iface.GetSSID() != "" {
|
||||
fmt.Printf(" SSID: %s\n", iface.GetSSID())
|
||||
}
|
||||
|
||||
if iface.GetSignal() != "" {
|
||||
fmt.Printf(" Signal: %s (%d%%)\n", iface.GetSignalDescription(), iface.GetSignalQuality())
|
||||
}
|
||||
|
||||
if iface.GetFrequencyKHz() > 0 {
|
||||
fmt.Printf(" Frequency: %s (%s)\n", iface.FormatFrequency(), iface.GetFrequencyBand())
|
||||
}
|
||||
|
||||
if iface.GetMode() != "" {
|
||||
fmt.Printf(" Mode: %s\n", iface.GetModeDescription())
|
||||
}
|
||||
}
|
||||
printNetworkInterface(i, &interfaces[i])
|
||||
}
|
||||
|
||||
// Show active connections summary
|
||||
|
||||
@@ -4,9 +4,30 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gesellix/bose-soundtouch/pkg/models"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func printSource(source models.SourceItem) {
|
||||
fmt.Printf(" • %s", source.GetDisplayName())
|
||||
|
||||
if source.SourceAccount != "" && source.SourceAccount != source.Source {
|
||||
fmt.Printf(" (%s)", source.SourceAccount)
|
||||
}
|
||||
|
||||
var attributes []string
|
||||
if source.IsLocalSource() {
|
||||
attributes = append(attributes, "Local")
|
||||
attributes = append(attributes, "Available")
|
||||
}
|
||||
|
||||
if len(attributes) > 0 {
|
||||
fmt.Printf(" [%s]", strings.Join(attributes, ", "))
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// listSources handles listing available audio sources
|
||||
func listSources(c *cli.Context) error {
|
||||
clientConfig := GetClientConfig(c)
|
||||
@@ -32,26 +53,7 @@ func listSources(c *cli.Context) error {
|
||||
fmt.Printf(" Ready Sources:\n")
|
||||
|
||||
for _, source := range availableSources {
|
||||
fmt.Printf(" • %s", source.GetDisplayName())
|
||||
|
||||
if source.SourceAccount != "" && source.SourceAccount != source.Source {
|
||||
fmt.Printf(" (%s)", source.SourceAccount)
|
||||
}
|
||||
|
||||
var attributes []string
|
||||
if source.IsLocalSource() {
|
||||
attributes = append(attributes, "Local")
|
||||
}
|
||||
|
||||
if source.IsLocalSource() {
|
||||
attributes = append(attributes, "Available")
|
||||
}
|
||||
|
||||
if len(attributes) > 0 {
|
||||
fmt.Printf(" [%s]", strings.Join(attributes, ", "))
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
printSource(source)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+225
-190
@@ -43,6 +43,88 @@ func parseHostPort(hostPort string, defaultPort int) (string, int) {
|
||||
return hostPort, defaultPort
|
||||
}
|
||||
|
||||
func parseFilters(eventFilter string) map[string]bool {
|
||||
validFilters := map[string]bool{
|
||||
"nowPlaying": true, "volume": true, "connection": true,
|
||||
"preset": true, "zone": true, "bass": true,
|
||||
}
|
||||
|
||||
if eventFilter == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
filters := make(map[string]bool)
|
||||
filterList := strings.Split(eventFilter, ",")
|
||||
|
||||
for _, f := range filterList {
|
||||
f = strings.TrimSpace(f)
|
||||
if !validFilters[f] {
|
||||
fmt.Printf("Invalid filter '%s'. Valid filters: nowPlaying, volume, connection, preset, zone, bass\n", f)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
filters[f] = true
|
||||
}
|
||||
|
||||
return filters
|
||||
}
|
||||
|
||||
func discoverDevice(discoverFlag bool, hostPort string, defaultPort int) (string, int, error) {
|
||||
if hostPort != "" && !discoverFlag {
|
||||
deviceHost, devicePort := parseHostPort(hostPort, defaultPort)
|
||||
fmt.Printf("Connecting to: %s:%d\n", deviceHost, devicePort)
|
||||
|
||||
return deviceHost, devicePort, nil
|
||||
}
|
||||
|
||||
fmt.Println("Discovering SoundTouch devices...")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cfg := &config.Config{
|
||||
DiscoveryTimeout: 10 * time.Second,
|
||||
CacheEnabled: false,
|
||||
}
|
||||
discoveryService := discovery.NewUnifiedDiscoveryService(cfg)
|
||||
|
||||
devices, err := discoveryService.DiscoverDevices(ctx)
|
||||
if err != nil || len(devices) == 0 {
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("discovery failed: %w", err)
|
||||
}
|
||||
|
||||
return "", 0, fmt.Errorf("no SoundTouch devices found")
|
||||
}
|
||||
|
||||
device := devices[0]
|
||||
fmt.Printf("Found %d device(s), connecting to: %s (%s:%d)\n",
|
||||
len(devices), device.Name, device.Host, device.Port)
|
||||
|
||||
return device.Host, device.Port, nil
|
||||
}
|
||||
|
||||
func setupWebSocket(soundTouchClient *client.Client, reconnect, verbose bool) *client.WebSocketClient {
|
||||
wsConfig := &client.WebSocketConfig{
|
||||
ReconnectInterval: 5 * time.Second,
|
||||
MaxReconnectAttempts: 0, // Unlimited if reconnect enabled
|
||||
PingInterval: 30 * time.Second,
|
||||
PongTimeout: 10 * time.Second,
|
||||
ReadBufferSize: 2048,
|
||||
WriteBufferSize: 2048,
|
||||
}
|
||||
|
||||
if verbose {
|
||||
wsConfig.Logger = &VerboseLogger{}
|
||||
}
|
||||
|
||||
if !reconnect {
|
||||
wsConfig.MaxReconnectAttempts = 1
|
||||
}
|
||||
|
||||
return soundTouchClient.NewWebSocketClient(wsConfig)
|
||||
}
|
||||
|
||||
func main() {
|
||||
var (
|
||||
host = flag.String("host", "", "SoundTouch device host/IP address (can include port like host:8090)")
|
||||
@@ -64,69 +146,13 @@ func main() {
|
||||
}
|
||||
|
||||
// Validate filter if provided
|
||||
validFilters := map[string]bool{
|
||||
"nowPlaying": true, "volume": true, "connection": true,
|
||||
"preset": true, "zone": true, "bass": true,
|
||||
}
|
||||
|
||||
var filters map[string]bool
|
||||
if *eventFilter != "" {
|
||||
filters = make(map[string]bool)
|
||||
|
||||
filterList := strings.Split(*eventFilter, ",")
|
||||
for _, f := range filterList {
|
||||
f = strings.TrimSpace(f)
|
||||
if !validFilters[f] {
|
||||
fmt.Printf("Invalid filter '%s'. Valid filters: nowPlaying, volume, connection, preset, zone, bass\n", f)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
filters[f] = true
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
deviceHost string
|
||||
devicePort int
|
||||
)
|
||||
filters := parseFilters(*eventFilter)
|
||||
|
||||
// Discover devices if no host specified or discover flag used
|
||||
|
||||
if *host == "" || *discover {
|
||||
fmt.Println("Discovering SoundTouch devices...")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Create unified discovery service
|
||||
cfg := &config.Config{
|
||||
DiscoveryTimeout: 10 * time.Second,
|
||||
CacheEnabled: false,
|
||||
}
|
||||
discoveryService := discovery.NewUnifiedDiscoveryService(cfg)
|
||||
|
||||
devices, err := discoveryService.DiscoverDevices(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf("Discovery failed: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(devices) == 0 {
|
||||
fmt.Println("No SoundTouch devices found")
|
||||
return
|
||||
}
|
||||
|
||||
// Use first discovered device
|
||||
device := devices[0]
|
||||
deviceHost = device.Host
|
||||
devicePort = device.Port
|
||||
|
||||
fmt.Printf("Found %d device(s), connecting to: %s (%s:%d)\n",
|
||||
len(devices), device.Name, device.Host, device.Port)
|
||||
} else {
|
||||
// Parse provided host
|
||||
deviceHost, devicePort = parseHostPort(*host, *port)
|
||||
fmt.Printf("Connecting to: %s:%d\n", deviceHost, devicePort)
|
||||
deviceHost, devicePort, err := discoverDevice(*discover, *host, *port)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create client
|
||||
@@ -156,24 +182,7 @@ func main() {
|
||||
deviceInfo.Name, deviceInfo.Type, macAddress)
|
||||
|
||||
// Create WebSocket client
|
||||
wsConfig := &client.WebSocketConfig{
|
||||
ReconnectInterval: 5 * time.Second,
|
||||
MaxReconnectAttempts: 0, // Unlimited if reconnect enabled
|
||||
PingInterval: 30 * time.Second,
|
||||
PongTimeout: 10 * time.Second,
|
||||
ReadBufferSize: 2048,
|
||||
WriteBufferSize: 2048,
|
||||
}
|
||||
|
||||
if *verbose {
|
||||
wsConfig.Logger = &VerboseLogger{}
|
||||
}
|
||||
|
||||
if !*reconnect {
|
||||
wsConfig.MaxReconnectAttempts = 1
|
||||
}
|
||||
|
||||
wsClient := soundTouchClient.NewWebSocketClient(wsConfig)
|
||||
wsClient := setupWebSocket(soundTouchClient, *reconnect, *verbose)
|
||||
|
||||
// Set up event handlers
|
||||
setupEventHandlers(wsClient, filters, *verbose)
|
||||
@@ -181,9 +190,10 @@ func main() {
|
||||
// Connect to WebSocket
|
||||
fmt.Println("Connecting to WebSocket...")
|
||||
|
||||
err = wsClient.ConnectWithConfig(wsConfig)
|
||||
err = wsClient.Connect()
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to connect to WebSocket: %v\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -243,150 +253,175 @@ func main() {
|
||||
fmt.Println("Disconnected successfully")
|
||||
}
|
||||
|
||||
func handleNowPlaying(event *models.NowPlayingUpdatedEvent, verbose bool) {
|
||||
fmt.Printf("\n🎵 Now Playing Update [%s]:\n", event.DeviceID)
|
||||
np := &event.NowPlaying
|
||||
|
||||
if np.IsEmpty() {
|
||||
fmt.Println(" ⏹️ Nothing playing")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" 🎵 %s\n", np.GetDisplayTitle())
|
||||
|
||||
if artist := np.GetDisplayArtist(); artist != "" {
|
||||
fmt.Printf(" 👤 %s\n", artist)
|
||||
}
|
||||
|
||||
if np.Album != "" {
|
||||
fmt.Printf(" 💿 %s\n", np.Album)
|
||||
}
|
||||
|
||||
fmt.Printf(" 📻 Source: %s\n", np.Source)
|
||||
fmt.Printf(" ▶️ Status: %s\n", np.PlayStatus.String())
|
||||
|
||||
if np.HasTimeInfo() {
|
||||
fmt.Printf(" ⏱️ Duration: %s\n", np.FormatDuration())
|
||||
}
|
||||
|
||||
if np.ShuffleSetting != "" {
|
||||
fmt.Printf(" 🔀 Shuffle: %s\n", np.ShuffleSetting.String())
|
||||
}
|
||||
|
||||
if np.RepeatSetting != "" {
|
||||
fmt.Printf(" 🔁 Repeat: %s\n", np.RepeatSetting.String())
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Printf(" 📱 Raw Source: %s, Account: %s\n", np.Source, np.SourceAccount)
|
||||
|
||||
if np.Art != nil && np.Art.URL != "" {
|
||||
fmt.Printf(" 🖼️ Artwork: %s\n", np.Art.URL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleVolume(event *models.VolumeUpdatedEvent, verbose bool) {
|
||||
vol := &event.Volume
|
||||
fmt.Printf("\n🔊 Volume Update [%s]:\n", event.DeviceID)
|
||||
|
||||
if vol.IsMuted() {
|
||||
fmt.Println(" 🔇 Muted")
|
||||
} else {
|
||||
fmt.Printf(" 🔊 Level: %d\n", vol.ActualVolume)
|
||||
|
||||
if vol.TargetVolume != vol.ActualVolume {
|
||||
fmt.Printf(" 🎯 Target: %d\n", vol.TargetVolume)
|
||||
}
|
||||
|
||||
fmt.Printf(" 📊 %s\n", models.GetVolumeLevelName(vol.ActualVolume))
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Printf(" 📱 Sync: %v\n", vol.IsVolumeSync())
|
||||
}
|
||||
}
|
||||
|
||||
func handleConnection(event *models.ConnectionStateUpdatedEvent) {
|
||||
cs := &event.ConnectionState
|
||||
fmt.Printf("\n🌐 Connection Update [%s]:\n", event.DeviceID)
|
||||
|
||||
if cs.IsConnected() {
|
||||
fmt.Println(" ✅ Connected")
|
||||
} else {
|
||||
fmt.Printf(" ❌ State: %s\n", cs.State)
|
||||
}
|
||||
|
||||
if cs.Signal != "" {
|
||||
fmt.Printf(" 📶 Signal: %s\n", cs.GetSignalStrength())
|
||||
}
|
||||
}
|
||||
|
||||
func handlePreset(event *models.PresetUpdatedEvent, verbose bool) {
|
||||
preset := &event.Preset
|
||||
fmt.Printf("\n📻 Preset Update [%s]:\n", event.DeviceID)
|
||||
fmt.Printf(" 📻 Preset: %d\n", preset.ID)
|
||||
|
||||
if preset.ContentItem != nil {
|
||||
fmt.Printf(" 🎵 %s\n", preset.ContentItem.ItemName)
|
||||
fmt.Printf(" 📻 Source: %s\n", preset.ContentItem.Source)
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Printf(" 📱 Raw preset data: ID=%d\n", preset.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func handleZone(event *models.ZoneUpdatedEvent) {
|
||||
zone := &event.Zone
|
||||
fmt.Printf("\n🏠 Zone Update [%s]:\n", event.DeviceID)
|
||||
fmt.Printf(" 👑 Master: %s\n", zone.Master)
|
||||
|
||||
if len(zone.Members) > 0 {
|
||||
fmt.Printf(" 👥 Members (%d):\n", len(zone.Members))
|
||||
|
||||
for i, member := range zone.Members {
|
||||
fmt.Printf(" %d. %s (%s)\n", i+1, member.DeviceID, member.IP)
|
||||
}
|
||||
} else {
|
||||
fmt.Println(" 👤 Single device (no zone)")
|
||||
}
|
||||
}
|
||||
|
||||
func handleBass(event *models.BassUpdatedEvent) {
|
||||
bass := &event.Bass
|
||||
fmt.Printf("\n🎵 Bass Update [%s]:\n", event.DeviceID)
|
||||
fmt.Printf(" 🎚️ Level: %d\n", bass.ActualBass)
|
||||
|
||||
if bass.TargetBass != bass.ActualBass {
|
||||
fmt.Printf(" 🎯 Target: %d\n", bass.TargetBass)
|
||||
}
|
||||
|
||||
levelDesc := "Neutral"
|
||||
if bass.ActualBass > 0 {
|
||||
levelDesc = "Boosted"
|
||||
} else if bass.ActualBass < 0 {
|
||||
levelDesc = "Reduced"
|
||||
}
|
||||
|
||||
fmt.Printf(" 📊 %s\n", levelDesc)
|
||||
}
|
||||
|
||||
func setupEventHandlers(wsClient *client.WebSocketClient, filters map[string]bool, verbose bool) {
|
||||
// Now Playing events
|
||||
if filters == nil || filters["nowPlaying"] {
|
||||
wsClient.OnNowPlaying(func(event *models.NowPlayingUpdatedEvent) {
|
||||
fmt.Printf("\n🎵 Now Playing Update [%s]:\n", event.DeviceID)
|
||||
np := &event.NowPlaying
|
||||
|
||||
if np.IsEmpty() {
|
||||
fmt.Println(" ⏹️ Nothing playing")
|
||||
} else {
|
||||
fmt.Printf(" 🎵 %s\n", np.GetDisplayTitle())
|
||||
|
||||
if artist := np.GetDisplayArtist(); artist != "" {
|
||||
fmt.Printf(" 👤 %s\n", artist)
|
||||
}
|
||||
|
||||
if np.Album != "" {
|
||||
fmt.Printf(" 💿 %s\n", np.Album)
|
||||
}
|
||||
|
||||
fmt.Printf(" 📻 Source: %s\n", np.Source)
|
||||
fmt.Printf(" ▶️ Status: %s\n", np.PlayStatus.String())
|
||||
|
||||
if np.HasTimeInfo() {
|
||||
fmt.Printf(" ⏱️ Duration: %s\n", np.FormatDuration())
|
||||
}
|
||||
|
||||
if np.ShuffleSetting != "" {
|
||||
fmt.Printf(" 🔀 Shuffle: %s\n", np.ShuffleSetting.String())
|
||||
}
|
||||
|
||||
if np.RepeatSetting != "" {
|
||||
fmt.Printf(" 🔁 Repeat: %s\n", np.RepeatSetting.String())
|
||||
}
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Printf(" 📱 Raw Source: %s, Account: %s\n", np.Source, np.SourceAccount)
|
||||
|
||||
if np.Art != nil && np.Art.URL != "" {
|
||||
fmt.Printf(" 🖼️ Artwork: %s\n", np.Art.URL)
|
||||
}
|
||||
}
|
||||
handleNowPlaying(event, verbose)
|
||||
})
|
||||
}
|
||||
|
||||
// Volume events
|
||||
if filters == nil || filters["volume"] {
|
||||
wsClient.OnVolumeUpdated(func(event *models.VolumeUpdatedEvent) {
|
||||
vol := &event.Volume
|
||||
fmt.Printf("\n🔊 Volume Update [%s]:\n", event.DeviceID)
|
||||
|
||||
if vol.IsMuted() {
|
||||
fmt.Println(" 🔇 Muted")
|
||||
} else {
|
||||
fmt.Printf(" 🔊 Level: %d\n", vol.ActualVolume)
|
||||
|
||||
if vol.TargetVolume != vol.ActualVolume {
|
||||
fmt.Printf(" 🎯 Target: %d\n", vol.TargetVolume)
|
||||
}
|
||||
|
||||
fmt.Printf(" 📊 %s\n", models.GetVolumeLevelName(vol.ActualVolume))
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Printf(" 📱 Sync: %v\n", vol.IsVolumeSync())
|
||||
}
|
||||
handleVolume(event, verbose)
|
||||
})
|
||||
}
|
||||
|
||||
// Connection state events
|
||||
if filters == nil || filters["connection"] {
|
||||
wsClient.OnConnectionState(func(event *models.ConnectionStateUpdatedEvent) {
|
||||
cs := &event.ConnectionState
|
||||
fmt.Printf("\n🌐 Connection Update [%s]:\n", event.DeviceID)
|
||||
|
||||
if cs.IsConnected() {
|
||||
fmt.Println(" ✅ Connected")
|
||||
} else {
|
||||
fmt.Printf(" ❌ State: %s\n", cs.State)
|
||||
}
|
||||
|
||||
if cs.Signal != "" {
|
||||
fmt.Printf(" 📶 Signal: %s\n", cs.GetSignalStrength())
|
||||
}
|
||||
handleConnection(event)
|
||||
})
|
||||
}
|
||||
|
||||
// Preset events
|
||||
if filters == nil || filters["preset"] {
|
||||
wsClient.OnPresetUpdated(func(event *models.PresetUpdatedEvent) {
|
||||
preset := &event.Preset
|
||||
fmt.Printf("\n📻 Preset Update [%s]:\n", event.DeviceID)
|
||||
fmt.Printf(" 📻 Preset: %d\n", preset.ID)
|
||||
|
||||
if preset.ContentItem != nil {
|
||||
fmt.Printf(" 🎵 %s\n", preset.ContentItem.ItemName)
|
||||
fmt.Printf(" 📻 Source: %s\n", preset.ContentItem.Source)
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Printf(" 📱 Raw preset data: ID=%d\n", preset.ID)
|
||||
}
|
||||
handlePreset(event, verbose)
|
||||
})
|
||||
}
|
||||
|
||||
// Zone/Multiroom events
|
||||
if filters == nil || filters["zone"] {
|
||||
wsClient.OnZoneUpdated(func(event *models.ZoneUpdatedEvent) {
|
||||
zone := &event.Zone
|
||||
fmt.Printf("\n🏠 Zone Update [%s]:\n", event.DeviceID)
|
||||
fmt.Printf(" 👑 Master: %s\n", zone.Master)
|
||||
|
||||
if len(zone.Members) > 0 {
|
||||
fmt.Printf(" 👥 Members (%d):\n", len(zone.Members))
|
||||
|
||||
for i, member := range zone.Members {
|
||||
fmt.Printf(" %d. %s (%s)\n", i+1, member.DeviceID, member.IP)
|
||||
}
|
||||
} else {
|
||||
fmt.Println(" 👤 Single device (no zone)")
|
||||
}
|
||||
handleZone(event)
|
||||
})
|
||||
}
|
||||
|
||||
// Bass events
|
||||
if filters == nil || filters["bass"] {
|
||||
wsClient.OnBassUpdated(func(event *models.BassUpdatedEvent) {
|
||||
bass := &event.Bass
|
||||
fmt.Printf("\n🎵 Bass Update [%s]:\n", event.DeviceID)
|
||||
fmt.Printf(" 🎚️ Level: %d\n", bass.ActualBass)
|
||||
|
||||
if bass.TargetBass != bass.ActualBass {
|
||||
fmt.Printf(" 🎯 Target: %d\n", bass.TargetBass)
|
||||
}
|
||||
|
||||
levelDesc := "Neutral"
|
||||
if bass.ActualBass > 0 {
|
||||
levelDesc = "Boosted"
|
||||
} else if bass.ActualBass < 0 {
|
||||
levelDesc = "Reduced"
|
||||
}
|
||||
|
||||
fmt.Printf(" 📊 %s\n", levelDesc)
|
||||
handleBass(event)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+64
-35
@@ -368,6 +368,68 @@ func (ws *WebSocketClient) handleMessage(data []byte) {
|
||||
ws.handleEvent(event)
|
||||
}
|
||||
|
||||
func (ws *WebSocketClient) dispatchTypedEvent(handlers *models.WebSocketEventHandlers, eventType models.WebSocketEventType, event *models.WebSocketEvent) bool {
|
||||
switch eventType {
|
||||
case models.EventTypeNowPlaying:
|
||||
if handlers.OnNowPlaying != nil && event.NowPlayingUpdated != nil {
|
||||
handlers.OnNowPlaying(event.NowPlayingUpdated)
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
case models.EventTypeVolumeUpdated:
|
||||
if handlers.OnVolumeUpdated != nil && event.VolumeUpdated != nil {
|
||||
handlers.OnVolumeUpdated(event.VolumeUpdated)
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
case models.EventTypeConnectionState:
|
||||
if handlers.OnConnectionState != nil && event.ConnectionStateUpdated != nil {
|
||||
handlers.OnConnectionState(event.ConnectionStateUpdated)
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
case models.EventTypePresetUpdated:
|
||||
if handlers.OnPresetUpdated != nil && event.PresetUpdated != nil {
|
||||
handlers.OnPresetUpdated(event.PresetUpdated)
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
default:
|
||||
return ws.dispatchTypedEventContinued(handlers, eventType, event)
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *WebSocketClient) dispatchTypedEventContinued(handlers *models.WebSocketEventHandlers, eventType models.WebSocketEventType, event *models.WebSocketEvent) bool {
|
||||
switch eventType {
|
||||
case models.EventTypeZoneUpdated:
|
||||
if handlers.OnZoneUpdated != nil && event.ZoneUpdated != nil {
|
||||
handlers.OnZoneUpdated(event.ZoneUpdated)
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
case models.EventTypeBassUpdated:
|
||||
if handlers.OnBassUpdated != nil && event.BassUpdated != nil {
|
||||
handlers.OnBassUpdated(event.BassUpdated)
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
case models.EventTypeRecentsUpdated:
|
||||
return true
|
||||
|
||||
case models.EventTypeLanguageUpdated:
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// handleEvent dispatches events to appropriate handlers
|
||||
func (ws *WebSocketClient) handleEvent(event *models.WebSocketEvent) {
|
||||
ws.mu.RLock()
|
||||
@@ -378,41 +440,8 @@ func (ws *WebSocketClient) handleEvent(event *models.WebSocketEvent) {
|
||||
hasKnownEvent := false
|
||||
|
||||
for _, eventType := range eventTypes {
|
||||
hasKnownEvent = true
|
||||
|
||||
switch eventType {
|
||||
case models.EventTypeNowPlaying:
|
||||
if handlers.OnNowPlaying != nil && event.NowPlayingUpdated != nil {
|
||||
handlers.OnNowPlaying(event.NowPlayingUpdated)
|
||||
}
|
||||
|
||||
case models.EventTypeVolumeUpdated:
|
||||
if handlers.OnVolumeUpdated != nil && event.VolumeUpdated != nil {
|
||||
handlers.OnVolumeUpdated(event.VolumeUpdated)
|
||||
}
|
||||
|
||||
case models.EventTypeConnectionState:
|
||||
if handlers.OnConnectionState != nil && event.ConnectionStateUpdated != nil {
|
||||
handlers.OnConnectionState(event.ConnectionStateUpdated)
|
||||
}
|
||||
|
||||
case models.EventTypePresetUpdated:
|
||||
if handlers.OnPresetUpdated != nil && event.PresetUpdated != nil {
|
||||
handlers.OnPresetUpdated(event.PresetUpdated)
|
||||
}
|
||||
|
||||
case models.EventTypeZoneUpdated:
|
||||
if handlers.OnZoneUpdated != nil && event.ZoneUpdated != nil {
|
||||
handlers.OnZoneUpdated(event.ZoneUpdated)
|
||||
}
|
||||
|
||||
case models.EventTypeBassUpdated:
|
||||
if handlers.OnBassUpdated != nil && event.BassUpdated != nil {
|
||||
handlers.OnBassUpdated(event.BassUpdated)
|
||||
}
|
||||
|
||||
default:
|
||||
hasKnownEvent = false
|
||||
if ws.dispatchTypedEvent(handlers, eventType, event) {
|
||||
hasKnownEvent = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,9 @@ func TestBalanceMarshalXML(t *testing.T) {
|
||||
t.Fatalf("MarshalXML failed: %v", err)
|
||||
}
|
||||
|
||||
encoder.Flush()
|
||||
if err := encoder.Flush(); err != nil {
|
||||
t.Fatalf("Flush failed: %v", err)
|
||||
}
|
||||
|
||||
// Convert to string for easier testing
|
||||
xmlStr := buf.String()
|
||||
@@ -57,10 +59,11 @@ func TestBalanceMarshalXML_PositiveValue(t *testing.T) {
|
||||
t.Fatalf("MarshalXML failed: %v", err)
|
||||
}
|
||||
|
||||
encoder.Flush()
|
||||
if err := encoder.Flush(); err != nil {
|
||||
t.Fatalf("Flush failed: %v", err)
|
||||
}
|
||||
|
||||
xmlStr := buf.String()
|
||||
|
||||
expectedElements := []string{
|
||||
`deviceID="ABCDEF123456"`,
|
||||
`<targetbalance>30</targetbalance>`,
|
||||
@@ -90,10 +93,11 @@ func TestBalanceMarshalXML_ZeroValue(t *testing.T) {
|
||||
t.Fatalf("MarshalXML failed: %v", err)
|
||||
}
|
||||
|
||||
encoder.Flush()
|
||||
if err := encoder.Flush(); err != nil {
|
||||
t.Fatalf("Flush failed: %v", err)
|
||||
}
|
||||
|
||||
xmlStr := buf.String()
|
||||
|
||||
expectedElements := []string{
|
||||
`deviceID="ZERO0000TEST"`,
|
||||
`<targetbalance>0</targetbalance>`,
|
||||
@@ -123,10 +127,11 @@ func TestBalanceMarshalXML_ExtremeValues(t *testing.T) {
|
||||
t.Fatalf("MarshalXML failed: %v", err)
|
||||
}
|
||||
|
||||
encoder.Flush()
|
||||
if err := encoder.Flush(); err != nil {
|
||||
t.Fatalf("Flush failed: %v", err)
|
||||
}
|
||||
|
||||
xmlStr := buf.String()
|
||||
|
||||
expectedElements := []string{
|
||||
`deviceID="EXTREME_TEST"`,
|
||||
`<targetbalance>-50</targetbalance>`,
|
||||
|
||||
@@ -230,9 +230,10 @@ func TestBassMarshalXML(t *testing.T) {
|
||||
t.Fatalf("MarshalXML failed: %v", err)
|
||||
}
|
||||
|
||||
encoder.Flush()
|
||||
if err := encoder.Flush(); err != nil {
|
||||
t.Fatalf("Flush failed: %v", err)
|
||||
}
|
||||
|
||||
// Convert to string for easier testing
|
||||
xmlStr := buf.String()
|
||||
|
||||
// Check that XML contains expected elements
|
||||
|
||||
+88
-73
@@ -334,83 +334,98 @@ func ParseWebSocketEvent(data []byte) (*WebSocketEvent, error) {
|
||||
return &event, nil
|
||||
}
|
||||
|
||||
func (e *WebSocketEvent) getFieldByEventType(eventType WebSocketEventType) interface{} {
|
||||
var field interface{}
|
||||
|
||||
switch eventType {
|
||||
case EventTypeNowPlaying:
|
||||
field = e.NowPlayingUpdated
|
||||
case EventTypeVolumeUpdated:
|
||||
field = e.VolumeUpdated
|
||||
case EventTypeConnectionState:
|
||||
field = e.ConnectionStateUpdated
|
||||
case EventTypePresetUpdated:
|
||||
field = e.PresetUpdated
|
||||
case EventTypeZoneUpdated:
|
||||
field = e.ZoneUpdated
|
||||
case EventTypeBassUpdated:
|
||||
field = e.BassUpdated
|
||||
case EventTypeClockTimeUpdated:
|
||||
field = e.ClockTimeUpdated
|
||||
case EventTypeClockDisplayUpdated:
|
||||
field = e.ClockDisplayUpdated
|
||||
case EventTypeNameUpdated:
|
||||
field = e.NameUpdated
|
||||
case EventTypeErrorUpdated:
|
||||
field = e.ErrorUpdated
|
||||
case EventTypeRecentsUpdated:
|
||||
field = e.RecentsUpdated
|
||||
case EventTypeLanguageUpdated:
|
||||
field = e.LanguageUpdated
|
||||
}
|
||||
|
||||
// Use reflection or a type-safe check to ensure we only return non-nil interfaces
|
||||
// In Go, an interface is nil only if both its type and value are nil.
|
||||
// If e.NowPlayingUpdated is a nil pointer, field will be a non-nil interface containing a nil pointer.
|
||||
// We need to return a literal nil if the field is empty to satisfy expectations.
|
||||
|
||||
if field == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// We know all these fields are pointers.
|
||||
// We can't easily check for nil pointer without reflection here in a generic way,
|
||||
// but we can restore the previous logic in a more compact way if needed.
|
||||
// Actually, the previous logic was: if e.NowPlayingUpdated != nil { return e.NowPlayingUpdated }
|
||||
// which returns a non-nil interface.
|
||||
|
||||
return field
|
||||
}
|
||||
|
||||
// isNil checks if an interface is nil or contains a nil pointer.
|
||||
func isNil(i interface{}) bool {
|
||||
if i == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
switch v := i.(type) {
|
||||
case *NowPlayingUpdatedEvent:
|
||||
return v == nil
|
||||
case *VolumeUpdatedEvent:
|
||||
return v == nil
|
||||
case *ConnectionStateUpdatedEvent:
|
||||
return v == nil
|
||||
case *PresetUpdatedEvent:
|
||||
return v == nil
|
||||
case *ZoneUpdatedEvent:
|
||||
return v == nil
|
||||
case *BassUpdatedEvent:
|
||||
return v == nil
|
||||
case *ClockTimeUpdatedEvent:
|
||||
return v == nil
|
||||
case *ClockDisplayUpdatedEvent:
|
||||
return v == nil
|
||||
case *NameUpdatedEvent:
|
||||
return v == nil
|
||||
case *ErrorUpdatedEvent:
|
||||
return v == nil
|
||||
case *RecentsUpdatedEvent:
|
||||
return v == nil
|
||||
case *LanguageUpdatedEvent:
|
||||
return v == nil
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// ParseTypedEvent attempts to parse a WebSocket event into a specific typed event
|
||||
func ParseTypedEvent[T any](event *WebSocketEvent, eventType WebSocketEventType) (T, error) {
|
||||
var result T
|
||||
|
||||
// Get the event directly from the parsed structure
|
||||
switch eventType {
|
||||
case EventTypeNowPlaying:
|
||||
if event.NowPlayingUpdated != nil {
|
||||
if typedResult, ok := interface{}(event.NowPlayingUpdated).(T); ok {
|
||||
return typedResult, nil
|
||||
}
|
||||
}
|
||||
case EventTypeVolumeUpdated:
|
||||
if event.VolumeUpdated != nil {
|
||||
if typedResult, ok := interface{}(event.VolumeUpdated).(T); ok {
|
||||
return typedResult, nil
|
||||
}
|
||||
}
|
||||
case EventTypeConnectionState:
|
||||
if event.ConnectionStateUpdated != nil {
|
||||
if typedResult, ok := interface{}(event.ConnectionStateUpdated).(T); ok {
|
||||
return typedResult, nil
|
||||
}
|
||||
}
|
||||
case EventTypePresetUpdated:
|
||||
if event.PresetUpdated != nil {
|
||||
if typedResult, ok := interface{}(event.PresetUpdated).(T); ok {
|
||||
return typedResult, nil
|
||||
}
|
||||
}
|
||||
case EventTypeZoneUpdated:
|
||||
if event.ZoneUpdated != nil {
|
||||
if typedResult, ok := interface{}(event.ZoneUpdated).(T); ok {
|
||||
return typedResult, nil
|
||||
}
|
||||
}
|
||||
case EventTypeBassUpdated:
|
||||
if event.BassUpdated != nil {
|
||||
if typedResult, ok := interface{}(event.BassUpdated).(T); ok {
|
||||
return typedResult, nil
|
||||
}
|
||||
}
|
||||
case EventTypeClockTimeUpdated:
|
||||
if event.ClockTimeUpdated != nil {
|
||||
if typedResult, ok := interface{}(event.ClockTimeUpdated).(T); ok {
|
||||
return typedResult, nil
|
||||
}
|
||||
}
|
||||
case EventTypeClockDisplayUpdated:
|
||||
if event.ClockDisplayUpdated != nil {
|
||||
if typedResult, ok := interface{}(event.ClockDisplayUpdated).(T); ok {
|
||||
return typedResult, nil
|
||||
}
|
||||
}
|
||||
case EventTypeNameUpdated:
|
||||
if event.NameUpdated != nil {
|
||||
if typedResult, ok := interface{}(event.NameUpdated).(T); ok {
|
||||
return typedResult, nil
|
||||
}
|
||||
}
|
||||
case EventTypeErrorUpdated:
|
||||
if event.ErrorUpdated != nil {
|
||||
if typedResult, ok := interface{}(event.ErrorUpdated).(T); ok {
|
||||
return typedResult, nil
|
||||
}
|
||||
}
|
||||
case EventTypeRecentsUpdated:
|
||||
if event.RecentsUpdated != nil {
|
||||
if typedResult, ok := interface{}(event.RecentsUpdated).(T); ok {
|
||||
return typedResult, nil
|
||||
}
|
||||
}
|
||||
case EventTypeLanguageUpdated:
|
||||
if event.LanguageUpdated != nil {
|
||||
if typedResult, ok := interface{}(event.LanguageUpdated).(T); ok {
|
||||
return typedResult, nil
|
||||
}
|
||||
field := event.getFieldByEventType(eventType)
|
||||
if !isNil(field) {
|
||||
if typedResult, ok := field.(T); ok {
|
||||
return typedResult, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user