package main import ( "fmt" "time" "github.com/gesellix/bose-soundtouch/pkg/models" "github.com/urfave/cli/v2" ) // getDeviceInfo handles the device info command func getDeviceInfo(c *cli.Context) error { clientConfig := GetClientConfig(c) client, err := CreateSoundTouchClient(clientConfig) if err != nil { return err } PrintDeviceHeader("Getting device information", clientConfig.Host, clientConfig.Port) deviceInfo, err := client.GetDeviceInfo() if err != nil { return fmt.Errorf("failed to get device info: %w", err) } // Display basic device information fmt.Printf("Device Information:\n") fmt.Printf(" Name: %s\n", deviceInfo.Name) fmt.Printf(" Type: %s\n", deviceInfo.Type) fmt.Printf(" Device ID: %s\n", deviceInfo.DeviceID) if deviceInfo.MargeAccountUUID != "" { fmt.Printf(" Account UUID: %s\n", deviceInfo.MargeAccountUUID) } if len(deviceInfo.NetworkInfo) > 0 { fmt.Printf(" Network Info:\n") for _, net := range deviceInfo.NetworkInfo { fmt.Printf(" - Type: %s\n", net.Type) fmt.Printf(" MAC Address: %s\n", net.MacAddress) fmt.Printf(" IP Address: %s\n", net.IPAddress) } } if len(deviceInfo.Components) > 0 { fmt.Printf(" Components:\n") for _, component := range deviceInfo.Components { fmt.Printf(" - Category: %s\n", component.ComponentCategory) if component.SoftwareVersion != "" { fmt.Printf(" Software Version: %s\n", component.SoftwareVersion) } if component.SerialNumber != "" { fmt.Printf(" Serial Number: %s\n", component.SerialNumber) } } } return nil } // getDeviceName handles getting the device name func getDeviceName(c *cli.Context) error { clientConfig := GetClientConfig(c) client, err := CreateSoundTouchClient(clientConfig) if err != nil { return err } PrintDeviceHeader("Getting device name", clientConfig.Host, clientConfig.Port) name, err := client.GetName() if err != nil { return fmt.Errorf("failed to get device name: %w", err) } fmt.Printf("Device Name: %s\n", name) return nil } // setDeviceName handles setting the device name func setDeviceName(c *cli.Context) error { clientConfig := GetClientConfig(c) client, err := CreateSoundTouchClient(clientConfig) if err != nil { return err } newName := c.String("value") if newName == "" { return fmt.Errorf("device name cannot be empty") } PrintDeviceHeader(fmt.Sprintf("Setting device name to '%s'", newName), clientConfig.Host, clientConfig.Port) err = client.SetName(newName) if err != nil { return fmt.Errorf("failed to set device name: %w", err) } PrintSuccess(fmt.Sprintf("Device name set to '%s'", newName)) return nil } // getCapabilities handles getting device capabilities func getCapabilities(c *cli.Context) error { clientConfig := GetClientConfig(c) client, err := CreateSoundTouchClient(clientConfig) if err != nil { return err } PrintDeviceHeader("Getting device capabilities", clientConfig.Host, clientConfig.Port) capabilities, err := client.GetCapabilities() if err != nil { return fmt.Errorf("failed to get capabilities: %w", err) } fmt.Printf("Device Capabilities:\n") fmt.Printf(" Device ID: %s\n", capabilities.DeviceID) // Network capabilities networkCaps := capabilities.GetNetworkCapabilities() if len(networkCaps) > 0 { fmt.Printf(" Network Capabilities:\n") for _, cap := range networkCaps { fmt.Printf(" - %s\n", cap) } } // Extended capabilities capNames := capabilities.GetCapabilityNames() if len(capNames) > 0 { fmt.Printf(" Extended Capabilities:\n") for _, capName := range capNames { capability := capabilities.GetCapabilityByName(capName) fmt.Printf(" - %s", capName) if capability.URL != "" { fmt.Printf(" (%s)", capability.URL) } fmt.Println() } } return nil } // getPresets handles getting device presets func getPresets(c *cli.Context) error { clientConfig := GetClientConfig(c) client, err := CreateSoundTouchClient(clientConfig) if err != nil { return err } PrintDeviceHeader("Getting device presets", clientConfig.Host, clientConfig.Port) presets, err := client.GetPresets() if err != nil { return fmt.Errorf("failed to get presets: %w", err) } fmt.Printf("Device Presets:\n") // Filter out placeholder presets the firmware emits for unconfigured // slots (issue #308): self-closing after factory reset, // or on healthy devices. // IsEmpty covers both shapes; accessing fields like ContentItem.Source // directly on the first shape panics. configured := make([]models.Preset, 0, len(presets.Preset)) for _, p := range presets.Preset { if !p.IsEmpty() { configured = append(configured, p) } } if len(configured) == 0 { fmt.Printf(" No presets configured\n") return nil } fmt.Printf(" Configured Presets:\n") for _, preset := range configured { fmt.Printf(" %d. %s\n", preset.ID, preset.GetDisplayName()) fmt.Printf(" Source: %s\n", preset.GetSource()) if account := preset.GetSourceAccount(); account != "" && account != preset.GetSource() { fmt.Printf(" Account: %s\n", account) } if location := preset.GetLocation(); location != "" { fmt.Printf(" Location: %s\n", location) } // Show preset creation time if available if preset.CreatedOn != nil && *preset.CreatedOn != 0 { createdTime := time.Unix(*preset.CreatedOn, 0) fmt.Printf(" Created: %s\n", createdTime.Format("2006-01-02 15:04:05")) } fmt.Println() } return nil } // getSupportedURLs handles getting supported URLs/endpoints func getSupportedURLs(c *cli.Context) error { clientConfig := GetClientConfig(c) PrintDeviceHeader("Getting supported URLs", clientConfig.Host, clientConfig.Port) client, err := CreateSoundTouchClient(clientConfig) if err != nil { PrintError(fmt.Sprintf("Failed to create client: %v", err)) return err } supportedURLs, err := client.GetSupportedURLs() if err != nil { PrintError(fmt.Sprintf("Failed to get supported URLs: %v", err)) return err } printSupportedURLs(supportedURLs, c) return nil } // printSupportedURLs formats and displays supported URLs information func printSupportedURLs(supportedURLs *models.SupportedURLsResponse, c *cli.Context) { verbose := c.Bool("verbose") showFeatures := c.Bool("features") fmt.Printf("Device Supported URLs:\n") fmt.Printf(" Device ID: %s\n", supportedURLs.DeviceID) fmt.Printf(" Total Endpoints: %d\n", supportedURLs.GetURLCount()) // Show feature completeness score completeness, supported, total := supportedURLs.GetFeatureCompleteness() fmt.Printf(" Feature Coverage: %d%% (%d/%d features)\n\n", completeness, supported, total) if showFeatures || (!verbose && !showFeatures) { // Show feature mapping (default view) printFeatureMapping(supportedURLs, verbose) } if verbose { fmt.Println() printDetailedEndpoints(supportedURLs) } if !showFeatures && !verbose { fmt.Printf("\nšŸ’” Options:\n") fmt.Printf(" --features Show detailed feature mapping and CLI commands\n") fmt.Printf(" --verbose Show complete endpoint list\n") } } // printFeatureMapping displays the feature-to-endpoint mapping func printFeatureMapping(supportedURLs *models.SupportedURLsResponse, verbose bool) { fmt.Printf("šŸŽÆ Device Feature Support:\n\n") // Get features organized by category featuresByCategory := supportedURLs.GetFeaturesByCategory() printFeatureCategories(featuresByCategory, supportedURLs, verbose) printMissingEssentialFeatures(supportedURLs) printPartiallyImplementedFeatures(supportedURLs, verbose) } func printFeatureCategories(featuresByCategory map[string][]models.EndpointFeature, supportedURLs *models.SupportedURLsResponse, verbose bool) { categoryInfo := map[string]string{ "Core": "⚔", "Audio": "šŸ”Š", "Playback": "ā–¶ļø", "Sources": "šŸ“±", "Content": "šŸ“»", "Presets": "⭐", "Multiroom": "šŸ ", "Network": "🌐", "System": "āš™ļø", } categoryOrder := []string{"Core", "Audio", "Playback", "Sources", "Content", "Presets", "Multiroom", "Network", "System"} for _, category := range categoryOrder { features := featuresByCategory[category] if len(features) == 0 { continue } emoji := categoryInfo[category] fmt.Printf("%s %s (%d features):\n", emoji, category, len(features)) for _, feature := range features { printFeatureStatus(feature, supportedURLs, verbose) } fmt.Println() } } func printFeatureStatus(feature models.EndpointFeature, supportedURLs *models.SupportedURLsResponse, verbose bool) { supportedEndpoints := countSupportedEndpoints(feature, supportedURLs) status := "āœ…" if supportedEndpoints < len(feature.Endpoints) && len(feature.Endpoints) > 1 { status = "āš ļø" // Partial support } fmt.Printf(" %s %s", status, feature.Name) if feature.Essential { fmt.Printf(" ⭐") } fmt.Printf("\n") if verbose { printVerboseFeatureDetails(feature, supportedEndpoints) } } func countSupportedEndpoints(feature models.EndpointFeature, supportedURLs *models.SupportedURLsResponse) int { supportedEndpoints := 0 for _, endpoint := range feature.Endpoints { if supportedURLs.HasURL(endpoint) { supportedEndpoints++ } } return supportedEndpoints } func printVerboseFeatureDetails(feature models.EndpointFeature, supportedEndpoints int) { fmt.Printf(" %s\n", feature.Description) fmt.Printf(" CLI: %s\n", feature.CLICommand) fmt.Printf(" Endpoints: %d/%d supported", supportedEndpoints, len(feature.Endpoints)) if supportedEndpoints < len(feature.Endpoints) { fmt.Printf(" (partial)") } fmt.Printf("\n") } func printMissingEssentialFeatures(supportedURLs *models.SupportedURLsResponse) { missingEssential := supportedURLs.GetMissingEssentialFeatures() if len(missingEssential) > 0 { fmt.Printf("āš ļø Missing Essential Features:\n") for _, feature := range missingEssential { fmt.Printf(" āŒ %s - %s\n", feature.Name, feature.Description) } fmt.Println() } } func printPartiallyImplementedFeatures(supportedURLs *models.SupportedURLsResponse, verbose bool) { partial := supportedURLs.GetPartiallyImplementedFeatures() if len(partial) > 0 && verbose { fmt.Printf("āš ļø Partially Supported Features:\n") for _, feature := range partial { fmt.Printf(" 🟔 %s\n", feature.Name) for _, endpoint := range feature.Endpoints { status := "āŒ" if supportedURLs.HasURL(endpoint) { status = "āœ…" } fmt.Printf(" %s %s\n", status, endpoint) } } fmt.Println() } } // printDetailedEndpoints shows the traditional endpoint listing func printDetailedEndpoints(supportedURLs *models.SupportedURLsResponse) { fmt.Printf("šŸ“‹ Detailed Endpoint Analysis:\n\n") // Show core functionality coreURLs := supportedURLs.GetCoreURLs() if len(coreURLs) > 0 { fmt.Printf("šŸŽ® Core Functionality (%d endpoints):\n", len(coreURLs)) for _, url := range coreURLs { fmt.Printf(" • %s\n", url) } fmt.Println() } // Show streaming functionality streamingURLs := supportedURLs.GetStreamingURLs() if len(streamingURLs) > 0 { fmt.Printf("šŸ“» Streaming Services (%d endpoints):\n", len(streamingURLs)) for _, url := range streamingURLs { fmt.Printf(" • %s\n", url) } fmt.Println() } // Show advanced audio functionality advancedURLs := supportedURLs.GetAdvancedURLs() if len(advancedURLs) > 0 { fmt.Printf("šŸ”§ Advanced Audio (%d endpoints):\n", len(advancedURLs)) for _, url := range advancedURLs { fmt.Printf(" • %s\n", url) } fmt.Println() } // Show network functionality networkURLs := supportedURLs.GetNetworkURLs() if len(networkURLs) > 0 { fmt.Printf("🌐 Network & Connectivity (%d endpoints):\n", len(networkURLs)) for _, url := range networkURLs { fmt.Printf(" • %s\n", url) } fmt.Println() } // Show all supported URLs fmt.Printf("šŸ“ Complete Endpoint List:\n") allURLs := supportedURLs.GetURLs() for i, url := range allURLs { fmt.Printf(" %3d. %s\n", i+1, url) } } // getDeviceAnalysis handles comprehensive device capability analysis func getDeviceAnalysis(c *cli.Context) error { clientConfig := GetClientConfig(c) PrintDeviceHeader("Analyzing device capabilities", clientConfig.Host, clientConfig.Port) client, err := CreateSoundTouchClient(clientConfig) if err != nil { PrintError(fmt.Sprintf("Failed to create client: %v", err)) return err } supportedURLs, err := client.GetSupportedURLs() if err != nil { PrintError(fmt.Sprintf("Failed to get supported URLs: %v", err)) return err } printDeviceAnalysis(supportedURLs) return nil } // printDeviceAnalysis provides comprehensive device capability analysis func printDeviceAnalysis(supportedURLs *models.SupportedURLsResponse) { fmt.Printf("šŸ” Device Capability Analysis:\n") fmt.Printf(" Device ID: %s\n", supportedURLs.DeviceID) // Overall score completeness, supported, total := supportedURLs.GetFeatureCompleteness() fmt.Printf(" Feature Coverage: %d%% (%d/%d features)\n", completeness, supported, total) // Device classification classification := classifyDevice(supportedURLs) fmt.Printf(" Device Type: %s\n\n", classification) // Essential features check missingEssential := supportedURLs.GetMissingEssentialFeatures() if len(missingEssential) > 0 { fmt.Printf("āŒ Missing Essential Features:\n") for _, feature := range missingEssential { fmt.Printf(" • %s - %s\n", feature.Name, feature.Description) fmt.Printf(" Impact: Device may not function properly without this\n") } fmt.Println() } else { fmt.Printf("āœ… All essential features are supported\n\n") } // Show what works supportedFeatures := supportedURLs.GetSupportedFeatures() fmt.Printf("āœ… Available Features (%d):\n", len(supportedFeatures)) categoryCount := make(map[string]int) for _, feature := range supportedFeatures { categoryCount[feature.Category]++ } for category, count := range categoryCount { emoji := getCategoryEmoji(category) fmt.Printf(" %s %s: %d features\n", emoji, category, count) } fmt.Println() // Show what's missing unsupportedFeatures := supportedURLs.GetUnsupportedFeatures() if len(unsupportedFeatures) > 0 { fmt.Printf("āŒ Unsupported Features (%d):\n", len(unsupportedFeatures)) for _, feature := range unsupportedFeatures { fmt.Printf(" • %s - %s\n", feature.Name, feature.Description) } fmt.Println() } // Partial implementations partial := supportedURLs.GetPartiallyImplementedFeatures() if len(partial) > 0 { fmt.Printf("āš ļø Partially Supported Features (%d):\n", len(partial)) for _, feature := range partial { supportedCount := 0 for _, endpoint := range feature.Endpoints { if supportedURLs.HasURL(endpoint) { supportedCount++ } } fmt.Printf(" • %s (%d/%d endpoints)\n", feature.Name, supportedCount, len(feature.Endpoints)) } fmt.Println() } // Recommendations printRecommendations(supportedURLs) // CLI usage suggestions printCLIUsageSuggestions(supportedURLs) } // classifyDevice determines the device type based on supported features func classifyDevice(supportedURLs *models.SupportedURLsResponse) string { if supportedURLs.HasMultiroomSupport() && supportedURLs.HasAdvancedAudioSupport() { return "Premium SoundTouch Speaker (Full Feature Set)" } if supportedURLs.HasMultiroomSupport() { return "Standard SoundTouch Speaker (Multiroom Capable)" } if supportedURLs.HasStreamingSupport() && supportedURLs.HasPresetSupport() { return "Basic SoundTouch Speaker" } if supportedURLs.HasCorePlaybackSupport() { return "Essential SoundTouch Device" } return "Limited SoundTouch Device" } // printRecommendations provides usage recommendations based on device capabilities func printRecommendations(supportedURLs *models.SupportedURLsResponse) { fmt.Printf("šŸ’” Recommendations:\n") if supportedURLs.HasMultiroomSupport() { fmt.Printf(" šŸ  This device supports multiroom - you can create speaker groups\n") fmt.Printf(" Try: soundtouch-cli zone create --master --members \n") } if supportedURLs.HasPresetSupport() { fmt.Printf(" ⭐ Save your favorite content as presets for quick access\n") fmt.Printf(" Try: soundtouch-cli preset store-current --slot 1\n") } if supportedURLs.HasStreamingSupport() { fmt.Printf(" šŸ“» Browse and discover new content from streaming services\n") fmt.Printf(" Try: soundtouch-cli browse tunein, station search-tunein --query jazz\n") } if supportedURLs.HasAdvancedAudioSupport() { fmt.Printf(" šŸ”§ Fine-tune your audio with advanced controls\n") fmt.Printf(" Try: soundtouch-cli audio dsp get, audio tone get\n") } if !supportedURLs.HasURL("/bassCapabilities") { fmt.Printf(" āš ļø Device may have limited bass control options\n") } if !supportedURLs.HasURL("/balance") { fmt.Printf(" āš ļø No balance control available on this device\n") } fmt.Println() } // printCLIUsageSuggestions shows common CLI commands for this device func printCLIUsageSuggestions(supportedURLs *models.SupportedURLsResponse) { fmt.Printf("šŸš€ Common Commands for This Device:\n") // Always available fmt.Printf(" • Get device info: soundtouch-cli info get\n") fmt.Printf(" • Control volume: soundtouch-cli volume set --level 50\n") if supportedURLs.HasURL("/nowPlaying") { fmt.Printf(" • Check what's playing: soundtouch-cli play now\n") } if supportedURLs.HasURL("/sources") { fmt.Printf(" • List audio sources: soundtouch-cli source list\n") } if supportedURLs.HasURL("/presets") { fmt.Printf(" • Manage presets: soundtouch-cli preset list\n") } if supportedURLs.HasURL("/bass") { fmt.Printf(" • Adjust bass: soundtouch-cli bass set --level 5\n") } if supportedURLs.HasURL("/setZone") { fmt.Printf(" • Create speaker group: soundtouch-cli zone create\n") } if supportedURLs.HasURL("/search") { fmt.Printf(" • Search content: soundtouch-cli station search-tunein --query \"classic rock\"\n") } fmt.Println() } // getCategoryEmoji returns emoji for feature categories func getCategoryEmoji(category string) string { emojis := map[string]string{ "Core": "⚔", "Audio": "šŸ”Š", "Playback": "ā–¶ļø", "Sources": "šŸ“±", "Content": "šŸ“»", "Presets": "⭐", "Multiroom": "šŸ ", "Network": "🌐", "System": "āš™ļø", } if emoji, exists := emojis[category]; exists { return emoji } return "šŸ“‹" } // getTrackInfo gets the track information func getTrackInfo(c *cli.Context) error { clientConfig := GetClientConfig(c) PrintDeviceHeader("Getting track information", clientConfig.Host, clientConfig.Port) fmt.Println("āš ļø WARNING: /trackInfo endpoint times out on real devices.") fmt.Println(" Use 'soundtouch-cli now' (playback status) command instead for track information.") client, err := CreateSoundTouchClient(clientConfig) if err != nil { PrintError(fmt.Sprintf("Failed to create client: %v", err)) return err } trackInfo, err := client.GetTrackInfo() if err != nil { PrintError(fmt.Sprintf("Failed to get track info: %v", err)) return err } fmt.Println("Track Information:") fmt.Printf(" Source: %s\n", trackInfo.Source) if trackInfo.Track != "" { fmt.Printf(" Track: %s\n", trackInfo.Track) } if trackInfo.Artist != "" { fmt.Printf(" Artist: %s\n", trackInfo.Artist) } if trackInfo.Album != "" { fmt.Printf(" Album: %s\n", trackInfo.Album) } if trackInfo.StationName != "" { fmt.Printf(" Station: %s\n", trackInfo.StationName) } fmt.Printf(" Play Status: %s\n", trackInfo.PlayStatus) return nil }