mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 00:56:16 +00:00
feat: implement comprehensive /supportedURLs endpoint with feature mapping system
✨ New Features: - Implement missing /supportedURLs endpoint with full XML parsing - Add comprehensive endpoint-to-feature mapping system (15+ features, 9 categories) - Create device capability analysis with personalized recommendations - Add intelligent device classification (Premium, Standard, Basic, Essential, Limited) 🔧 CLI Enhancements: - Add 'supported-urls' command with --features and --verbose flags - Add 'analyze' command for comprehensive device capability analysis - Add 'station list' command for saved station management - Add 'source availability' and 'source compare' commands - Enhanced service availability checking across all commands 📚 Models & API: - New SupportedURLsResponse model with rich helper methods - Enhanced ServiceAvailability model with validation utilities - New EndpointFeature mapping system with CLI command references - Feature completeness scoring and partial implementation detection 🧪 Testing: - 35+ new test cases covering all functionality - Comprehensive feature mapping validation tests - Service availability integration tests with real device scenarios - Mock server tests for error handling and edge cases 📖 Documentation: - New FEATURE-MAPPING-GUIDE.md with comprehensive usage examples - Updated API documentation with correct implementation status - CLI command reference organized by feature category - Device troubleshooting guide with capability checking 🎯 Key Capabilities: - Device feature coverage scoring (0-100%) - Essential vs optional feature classification - Personalized CLI command recommendations - Missing capability detection with usage impact analysis - Smart device type classification based on supported endpoints This resolves the documentation inconsistency where /supportedURLs was marked as implemented but was actually missing from the client. The new implementation goes far beyond basic endpoint listing to provide intelligent device capability analysis and personalized usage recommendations.
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gesellix/bose-soundtouch/pkg/models"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
@@ -207,11 +208,10 @@ func getPresets(c *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// selectPreset selects a preset by number (1-6)
|
||||
func selectPreset(c *cli.Context) error {
|
||||
presetNum := c.Int("preset")
|
||||
// getSupportedURLs handles getting supported URLs/endpoints
|
||||
func getSupportedURLs(c *cli.Context) error {
|
||||
clientConfig := GetClientConfig(c)
|
||||
PrintDeviceHeader(fmt.Sprintf("Selecting preset %d", presetNum), clientConfig.Host, clientConfig.Port)
|
||||
PrintDeviceHeader("Getting supported URLs", clientConfig.Host, clientConfig.Port)
|
||||
|
||||
client, err := CreateSoundTouchClient(clientConfig)
|
||||
if err != nil {
|
||||
@@ -219,17 +219,412 @@ func selectPreset(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = client.SelectPreset(presetNum)
|
||||
supportedURLs, err := client.GetSupportedURLs()
|
||||
if err != nil {
|
||||
PrintError(fmt.Sprintf("Failed to select preset: %v", err))
|
||||
PrintError(fmt.Sprintf("Failed to get supported URLs: %v", err))
|
||||
return err
|
||||
}
|
||||
|
||||
PrintSuccess(fmt.Sprintf("Preset %d selected", presetNum))
|
||||
|
||||
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("❌ Unavailable 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 <this-device> --members <other-devices>\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)
|
||||
|
||||
@@ -133,7 +133,6 @@ func TestShouldShowContentDetails(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContentDetailsDisplayLogic(t *testing.T) {
|
||||
// Test the specific conditions that determine when to show content details
|
||||
tests := []struct {
|
||||
|
||||
@@ -89,6 +89,11 @@ func listSources(c *cli.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Show service availability summary
|
||||
fmt.Println()
|
||||
checker := NewServiceAvailabilityChecker(client)
|
||||
checker.PrintServiceAvailabilitySummary()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -104,6 +109,14 @@ func selectSource(c *cli.Context) error {
|
||||
sourceName := strings.ToUpper(c.String("source"))
|
||||
sourceAccount := c.String("account")
|
||||
|
||||
// Check service availability
|
||||
checker := NewServiceAvailabilityChecker(client)
|
||||
|
||||
actionDescription := fmt.Sprintf("select %s source", strings.ToLower(sourceName))
|
||||
if !checker.CheckSourceAvailable(sourceName, actionDescription) {
|
||||
return fmt.Errorf("source '%s' is not available", sourceName)
|
||||
}
|
||||
|
||||
PrintDeviceHeader(fmt.Sprintf("Selecting source '%s'", sourceName), clientConfig.Host, clientConfig.Port)
|
||||
|
||||
err = client.SelectSource(sourceName, sourceAccount)
|
||||
@@ -129,6 +142,12 @@ func selectSpotify(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check Spotify availability
|
||||
checker := NewServiceAvailabilityChecker(client)
|
||||
if !checker.ValidateSpotifyAvailable("select Spotify source") {
|
||||
return fmt.Errorf("spotify is not available on this device")
|
||||
}
|
||||
|
||||
PrintDeviceHeader("Selecting Spotify source", clientConfig.Host, clientConfig.Port)
|
||||
|
||||
err = client.SelectSpotify("")
|
||||
@@ -150,6 +169,12 @@ func selectBluetooth(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check Bluetooth availability
|
||||
checker := NewServiceAvailabilityChecker(client)
|
||||
if !checker.ValidateBluetoothAvailable("select Bluetooth source") {
|
||||
return fmt.Errorf("bluetooth is not available on this device")
|
||||
}
|
||||
|
||||
PrintDeviceHeader("Selecting Bluetooth source", clientConfig.Host, clientConfig.Port)
|
||||
|
||||
err = client.SelectBluetooth()
|
||||
@@ -182,3 +207,178 @@ func selectAux(c *cli.Context) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getServiceAvailability handles displaying service availability information
|
||||
func getServiceAvailability(c *cli.Context) error {
|
||||
clientConfig := GetClientConfig(c)
|
||||
|
||||
client, err := CreateSoundTouchClient(clientConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
PrintDeviceHeader("Getting service availability", clientConfig.Host, clientConfig.Port)
|
||||
|
||||
serviceAvailability, err := client.GetServiceAvailability()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get service availability: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Service Availability Report:\n")
|
||||
fmt.Printf(" Total Services: %d\n", serviceAvailability.GetServiceCount())
|
||||
fmt.Printf(" Available Services: %d\n", serviceAvailability.GetAvailableServiceCount())
|
||||
fmt.Printf(" Unavailable Services: %d\n", serviceAvailability.GetUnavailableServiceCount())
|
||||
|
||||
// Show available services
|
||||
fmt.Printf("\n✅ Available Services:\n")
|
||||
availableServices := serviceAvailability.GetAvailableServices()
|
||||
if len(availableServices) == 0 {
|
||||
fmt.Printf(" None\n")
|
||||
} else {
|
||||
for _, service := range availableServices {
|
||||
fmt.Printf(" • %s\n", formatServiceTypeForDisplay(models.ServiceType(service.Type)))
|
||||
}
|
||||
}
|
||||
|
||||
// Show unavailable services with reasons
|
||||
fmt.Printf("\n❌ Unavailable Services:\n")
|
||||
unavailableServices := serviceAvailability.GetUnavailableServices()
|
||||
if len(unavailableServices) == 0 {
|
||||
fmt.Printf(" None\n")
|
||||
} else {
|
||||
for _, service := range unavailableServices {
|
||||
reason := ""
|
||||
if service.Reason != "" {
|
||||
reason = fmt.Sprintf(" (%s)", service.Reason)
|
||||
}
|
||||
fmt.Printf(" • %s%s\n", formatServiceTypeForDisplay(models.ServiceType(service.Type)), reason)
|
||||
}
|
||||
}
|
||||
|
||||
// Show service categories
|
||||
fmt.Printf("\n🎵 Streaming Services:\n")
|
||||
streamingServices := serviceAvailability.GetStreamingServices()
|
||||
availableCount := 0
|
||||
for _, service := range streamingServices {
|
||||
status := "❌"
|
||||
if service.IsAvailable {
|
||||
status = "✅"
|
||||
availableCount++
|
||||
}
|
||||
fmt.Printf(" %s %s\n", status, formatServiceTypeForDisplay(models.ServiceType(service.Type)))
|
||||
}
|
||||
fmt.Printf(" Summary: %d/%d streaming services available\n", availableCount, len(streamingServices))
|
||||
|
||||
fmt.Printf("\n🔗 Local Input Services:\n")
|
||||
localServices := serviceAvailability.GetLocalServices()
|
||||
localAvailableCount := 0
|
||||
for _, service := range localServices {
|
||||
status := "❌"
|
||||
if service.IsAvailable {
|
||||
status = "✅"
|
||||
localAvailableCount++
|
||||
}
|
||||
fmt.Printf(" %s %s\n", status, formatServiceTypeForDisplay(models.ServiceType(service.Type)))
|
||||
}
|
||||
fmt.Printf(" Summary: %d/%d local services available\n", localAvailableCount, len(localServices))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// compareSourcesAndAvailability compares configured sources with service availability
|
||||
func compareSourcesAndAvailability(c *cli.Context) error {
|
||||
clientConfig := GetClientConfig(c)
|
||||
|
||||
client, err := CreateSoundTouchClient(clientConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
PrintDeviceHeader("Comparing sources and service availability", clientConfig.Host, clientConfig.Port)
|
||||
|
||||
// Get both sources and service availability
|
||||
sources, err := client.GetSources()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get sources: %w", err)
|
||||
}
|
||||
|
||||
serviceAvailability, err := client.GetServiceAvailability()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get service availability: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Source vs Availability Comparison:\n\n")
|
||||
|
||||
// Check key services
|
||||
comparisons := []struct {
|
||||
name string
|
||||
configuredCheck func() bool
|
||||
availableCheck func() bool
|
||||
getConfiguredSources func() []models.SourceItem
|
||||
}{
|
||||
{
|
||||
"Spotify",
|
||||
sources.HasSpotify,
|
||||
serviceAvailability.HasSpotify,
|
||||
sources.GetSpotifySources,
|
||||
},
|
||||
{
|
||||
"Bluetooth",
|
||||
sources.HasBluetooth,
|
||||
serviceAvailability.HasBluetooth,
|
||||
func() []models.SourceItem { return sources.GetSourcesByType("BLUETOOTH") },
|
||||
},
|
||||
}
|
||||
|
||||
for _, comp := range comparisons {
|
||||
configured := comp.configuredCheck()
|
||||
available := comp.availableCheck()
|
||||
|
||||
fmt.Printf("🔍 %s:\n", comp.name)
|
||||
fmt.Printf(" Configured: %s\n", boolToStatus(configured))
|
||||
fmt.Printf(" Available: %s\n", boolToStatus(available))
|
||||
|
||||
switch {
|
||||
case available && !configured:
|
||||
fmt.Printf(" 💡 %s is available but not configured - consider setting it up\n", comp.name)
|
||||
case configured && !available:
|
||||
fmt.Printf(" ⚠️ %s is configured but not available - check device status\n", comp.name)
|
||||
|
||||
// Show specific reason if available
|
||||
switch comp.name {
|
||||
case "Spotify":
|
||||
service := serviceAvailability.GetServiceByType(models.ServiceTypeSpotify)
|
||||
if service != nil && service.Reason != "" {
|
||||
fmt.Printf(" 📝 Reason: %s\n", service.Reason)
|
||||
}
|
||||
case "Bluetooth":
|
||||
service := serviceAvailability.GetServiceByType(models.ServiceTypeBluetooth)
|
||||
if service != nil && service.Reason != "" {
|
||||
fmt.Printf(" 📝 Reason: %s\n", service.Reason)
|
||||
}
|
||||
}
|
||||
case configured && available:
|
||||
fmt.Printf(" ✅ %s is properly configured and available\n", comp.name)
|
||||
default:
|
||||
fmt.Printf(" ➖ %s is neither configured nor available\n", comp.name)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// Summary
|
||||
fmt.Printf("📊 Summary:\n")
|
||||
fmt.Printf(" Total configured sources: %d\n", sources.GetSourceCount())
|
||||
fmt.Printf(" Ready configured sources: %d\n", sources.GetReadySourceCount())
|
||||
fmt.Printf(" Total available services: %d\n", serviceAvailability.GetAvailableServiceCount())
|
||||
fmt.Printf(" Total possible services: %d\n", serviceAvailability.GetServiceCount())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// boolToStatus converts boolean to user-friendly status
|
||||
func boolToStatus(b bool) string {
|
||||
if b {
|
||||
return "✅ Yes"
|
||||
}
|
||||
return "❌ No"
|
||||
}
|
||||
|
||||
@@ -28,6 +28,13 @@ func searchStations(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check service availability for the source
|
||||
checker := NewServiceAvailabilityChecker(client)
|
||||
actionDescription := fmt.Sprintf("search %s stations", source)
|
||||
if !checker.CheckSourceAvailable(source, actionDescription) {
|
||||
return fmt.Errorf("source '%s' is not available for station search", source)
|
||||
}
|
||||
|
||||
response, err := client.SearchStation(source, sourceAccount, searchTerm)
|
||||
if err != nil {
|
||||
PrintError(fmt.Sprintf("Failed to search stations: %v", err))
|
||||
@@ -56,6 +63,12 @@ func searchTuneIn(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check TuneIn availability
|
||||
checker := NewServiceAvailabilityChecker(client)
|
||||
if !checker.ValidateTuneInAvailable("search TuneIn stations") {
|
||||
return fmt.Errorf("TuneIn is not available on this device")
|
||||
}
|
||||
|
||||
response, err := client.SearchTuneInStations(searchTerm)
|
||||
if err != nil {
|
||||
PrintError(fmt.Sprintf("Failed to search TuneIn: %v", err))
|
||||
@@ -90,6 +103,12 @@ func searchPandora(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check Pandora availability
|
||||
checker := NewServiceAvailabilityChecker(client)
|
||||
if !checker.ValidatePandoraAvailable("search Pandora stations") {
|
||||
return fmt.Errorf("pandora is not available on this device")
|
||||
}
|
||||
|
||||
response, err := client.SearchPandoraStations(sourceAccount, searchTerm)
|
||||
if err != nil {
|
||||
PrintError(fmt.Sprintf("Failed to search Pandora: %v", err))
|
||||
@@ -124,6 +143,12 @@ func searchSpotify(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check Spotify availability
|
||||
checker := NewServiceAvailabilityChecker(client)
|
||||
if !checker.ValidateSpotifyAvailable("search Spotify content") {
|
||||
return fmt.Errorf("Spotify is not available on this device")
|
||||
}
|
||||
|
||||
response, err := client.SearchSpotifyContent(sourceAccount, searchTerm)
|
||||
if err != nil {
|
||||
PrintError(fmt.Sprintf("Failed to search Spotify: %v", err))
|
||||
@@ -165,6 +190,13 @@ func addStation(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check service availability for the source
|
||||
checker := NewServiceAvailabilityChecker(client)
|
||||
actionDescription := fmt.Sprintf("add %s station", source)
|
||||
if !checker.CheckSourceAvailable(source, actionDescription) {
|
||||
return fmt.Errorf("source '%s' is not available for adding stations", source)
|
||||
}
|
||||
|
||||
err = client.AddStation(source, sourceAccount, token, name)
|
||||
if err != nil {
|
||||
PrintError(fmt.Sprintf("Failed to add station: %v", err))
|
||||
@@ -237,7 +269,8 @@ func printSearchResults(response *models.SearchStationResponse, searchTerm strin
|
||||
|
||||
if len(songs) > 0 {
|
||||
fmt.Printf("\n 🎵 Songs (%d):\n", len(songs))
|
||||
for i, song := range songs {
|
||||
for i := range songs {
|
||||
song := &songs[i]
|
||||
fmt.Printf(" %d. %s\n", i+1, song.GetDisplayName())
|
||||
if song.Artist != "" {
|
||||
fmt.Printf(" Artist: %s\n", song.Artist)
|
||||
@@ -255,7 +288,8 @@ func printSearchResults(response *models.SearchStationResponse, searchTerm strin
|
||||
|
||||
if len(artists) > 0 {
|
||||
fmt.Printf(" 🎤 Artists (%d):\n", len(artists))
|
||||
for i, artist := range artists {
|
||||
for i := range artists {
|
||||
artist := &artists[i]
|
||||
fmt.Printf(" %d. %s\n", i+1, artist.GetDisplayName())
|
||||
if artist.SourceAccount != "" {
|
||||
fmt.Printf(" Account: %s\n", artist.SourceAccount)
|
||||
@@ -267,7 +301,8 @@ func printSearchResults(response *models.SearchStationResponse, searchTerm strin
|
||||
|
||||
if len(stations) > 0 {
|
||||
fmt.Printf(" 📻 Stations (%d):\n", len(stations))
|
||||
for i, station := range stations {
|
||||
for i := range stations {
|
||||
station := &stations[i]
|
||||
fmt.Printf(" %d. %s\n", i+1, station.GetDisplayName())
|
||||
if station.SourceAccount != "" {
|
||||
fmt.Printf(" Account: %s\n", station.SourceAccount)
|
||||
@@ -294,43 +329,96 @@ func printSearchResults(response *models.SearchStationResponse, searchTerm strin
|
||||
// hasAccountResults checks if any results have source accounts
|
||||
func hasAccountResults(response *models.SearchStationResponse) bool {
|
||||
allResults := response.GetAllResults()
|
||||
for _, result := range allResults {
|
||||
if result.SourceAccount != "" {
|
||||
for i := range allResults {
|
||||
if allResults[i].SourceAccount != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// validateStationSource validates that the source is supported for station operations
|
||||
func validateStationSource(source string) error {
|
||||
if source == "" {
|
||||
return fmt.Errorf("source is required")
|
||||
// listStations handles listing saved stations
|
||||
func listStations(c *cli.Context) error {
|
||||
source := c.String("source")
|
||||
sourceAccount := c.String("source-account")
|
||||
|
||||
clientConfig := GetClientConfig(c)
|
||||
PrintDeviceHeader(fmt.Sprintf("Getting %s stations", source), clientConfig.Host, clientConfig.Port)
|
||||
|
||||
client, err := CreateSoundTouchClient(clientConfig)
|
||||
if err != nil {
|
||||
PrintError(fmt.Sprintf("Failed to create client: %v", err))
|
||||
return err
|
||||
}
|
||||
|
||||
validSources := []string{"TUNEIN", "PANDORA", "SPOTIFY"}
|
||||
for _, validSource := range validSources {
|
||||
if strings.EqualFold(source, validSource) {
|
||||
return nil
|
||||
// Check service availability for the source
|
||||
checker := NewServiceAvailabilityChecker(client)
|
||||
|
||||
actionDescription := fmt.Sprintf("list %s stations", source)
|
||||
if !checker.CheckSourceAvailable(source, actionDescription) {
|
||||
return fmt.Errorf("source '%s' is not available for listing stations", source)
|
||||
}
|
||||
|
||||
var response *models.NavigateResponse
|
||||
|
||||
switch strings.ToUpper(source) {
|
||||
case "TUNEIN":
|
||||
response, err = client.GetTuneInStations(sourceAccount)
|
||||
case "PANDORA":
|
||||
if sourceAccount == "" {
|
||||
PrintError("Pandora source account is required")
|
||||
return fmt.Errorf("source account required for Pandora")
|
||||
}
|
||||
response, err = client.GetPandoraStations(sourceAccount)
|
||||
default:
|
||||
return fmt.Errorf("listing stations is not supported for source: %s", source)
|
||||
}
|
||||
|
||||
return fmt.Errorf("invalid source: %s (valid sources: %v)", source, validSources)
|
||||
}
|
||||
|
||||
// formatStationToken formats a station token for display (truncate if too long)
|
||||
func formatStationToken(token string) string {
|
||||
if len(token) <= 50 {
|
||||
return token
|
||||
if err != nil {
|
||||
PrintError(fmt.Sprintf("Failed to get stations: %v", err))
|
||||
return err
|
||||
}
|
||||
return token[:47] + "..."
|
||||
|
||||
printStationList(response, source)
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractStationInfo extracts key information from a search result for display
|
||||
func extractStationInfo(result *models.SearchResult) (string, string, string) {
|
||||
name := result.GetDisplayName()
|
||||
token := result.Token
|
||||
description := result.Description
|
||||
// printStationList formats and displays saved station results
|
||||
func printStationList(response *models.NavigateResponse, source string) {
|
||||
fmt.Printf("Saved %s Stations:\n", source)
|
||||
|
||||
return name, token, description
|
||||
if response.TotalItems == 0 {
|
||||
fmt.Printf(" No stations found\n")
|
||||
return
|
||||
}
|
||||
|
||||
stations := response.GetStations()
|
||||
fmt.Printf(" Total stations: %d\n", response.TotalItems)
|
||||
fmt.Printf(" Showing: %d\n\n", len(stations))
|
||||
|
||||
for i, station := range stations {
|
||||
fmt.Printf(" %d. %s\n", i+1, station.Name)
|
||||
|
||||
if station.ContentItem != nil {
|
||||
if station.ContentItem.Location != "" {
|
||||
fmt.Printf(" Location: %s\n", station.ContentItem.Location)
|
||||
}
|
||||
if station.ContentItem.SourceAccount != "" {
|
||||
fmt.Printf(" Account: %s\n", station.ContentItem.SourceAccount)
|
||||
}
|
||||
if station.ContentItem.IsPresetable {
|
||||
fmt.Printf(" Can be saved as preset: Yes\n")
|
||||
}
|
||||
}
|
||||
|
||||
if station.Type != "" {
|
||||
fmt.Printf(" Type: %s\n", station.Type)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// Show usage hints
|
||||
fmt.Printf("💡 Usage hints:\n")
|
||||
fmt.Printf(" • To play a station: Use the location value with 'play content' command\n")
|
||||
fmt.Printf(" • To save as preset: Use 'preset set' command with the location\n")
|
||||
}
|
||||
|
||||
@@ -178,6 +178,32 @@ func main() {
|
||||
Action: getCapabilities,
|
||||
Before: RequireHost,
|
||||
},
|
||||
{
|
||||
Name: "supported-urls",
|
||||
Aliases: []string{"urls"},
|
||||
Usage: "Get supported device endpoints",
|
||||
Action: getSupportedURLs,
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "verbose",
|
||||
Aliases: []string{"v"},
|
||||
Usage: "Show complete endpoint list",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "features",
|
||||
Aliases: []string{"f"},
|
||||
Usage: "Show detailed feature mapping and CLI commands",
|
||||
},
|
||||
},
|
||||
Before: RequireHost,
|
||||
},
|
||||
{
|
||||
Name: "analyze",
|
||||
Aliases: []string{"analysis"},
|
||||
Usage: "Analyze device capabilities and provide recommendations",
|
||||
Action: getDeviceAnalysis,
|
||||
Before: RequireHost,
|
||||
},
|
||||
{
|
||||
Name: "presets",
|
||||
Usage: "Get configured presets",
|
||||
@@ -616,6 +642,23 @@ func main() {
|
||||
},
|
||||
Before: RequireHost,
|
||||
},
|
||||
{
|
||||
Name: "list",
|
||||
Usage: "List saved stations",
|
||||
Action: listStations,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "source",
|
||||
Usage: "Station source (TUNEIN, PANDORA)",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "source-account",
|
||||
Usage: "Source account (required for Pandora)",
|
||||
},
|
||||
},
|
||||
Before: RequireHost,
|
||||
},
|
||||
},
|
||||
},
|
||||
// Key commands
|
||||
@@ -788,6 +831,18 @@ func main() {
|
||||
Action: selectAux,
|
||||
Before: RequireHost,
|
||||
},
|
||||
{
|
||||
Name: "availability",
|
||||
Usage: "Show service availability",
|
||||
Action: getServiceAvailability,
|
||||
Before: RequireHost,
|
||||
},
|
||||
{
|
||||
Name: "compare",
|
||||
Usage: "Compare sources and service availability",
|
||||
Action: compareSourcesAndAvailability,
|
||||
Before: RequireHost,
|
||||
},
|
||||
},
|
||||
},
|
||||
// Bass commands
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gesellix/bose-soundtouch/pkg/client"
|
||||
"github.com/gesellix/bose-soundtouch/pkg/models"
|
||||
)
|
||||
|
||||
// ServiceAvailabilityChecker provides service availability validation for CLI commands
|
||||
type ServiceAvailabilityChecker struct {
|
||||
client *client.Client
|
||||
serviceAvailability *models.ServiceAvailability
|
||||
skipAvailabilityCheck bool
|
||||
cached bool
|
||||
}
|
||||
|
||||
// NewServiceAvailabilityChecker creates a new service availability checker
|
||||
func NewServiceAvailabilityChecker(client *client.Client) *ServiceAvailabilityChecker {
|
||||
skipCheck := os.Getenv("SOUNDTOUCH_SKIP_AVAILABILITY_CHECK") == "true" ||
|
||||
os.Getenv("SOUNDTOUCH_SKIP_AVAILABILITY_CHECK") == "1"
|
||||
|
||||
return &ServiceAvailabilityChecker{
|
||||
client: client,
|
||||
skipAvailabilityCheck: skipCheck,
|
||||
cached: false,
|
||||
}
|
||||
}
|
||||
|
||||
// loadServiceAvailability loads service availability data (cached after first call)
|
||||
func (sac *ServiceAvailabilityChecker) loadServiceAvailability() {
|
||||
if sac.cached {
|
||||
return
|
||||
}
|
||||
|
||||
if sac.skipAvailabilityCheck {
|
||||
// Create a mock availability that allows everything
|
||||
sac.serviceAvailability = &models.ServiceAvailability{}
|
||||
sac.cached = true
|
||||
return
|
||||
}
|
||||
|
||||
serviceAvailability, err := sac.client.GetServiceAvailability()
|
||||
if err != nil {
|
||||
// If availability check fails, warn but don't fail the command
|
||||
PrintWarning(fmt.Sprintf("Could not check service availability: %v", err))
|
||||
if !sac.skipAvailabilityCheck {
|
||||
PrintWarning("Command will proceed without availability validation")
|
||||
PrintWarning("Set SOUNDTOUCH_SKIP_AVAILABILITY_CHECK=true to disable these checks")
|
||||
}
|
||||
// Create empty availability to prevent further errors
|
||||
sac.serviceAvailability = &models.ServiceAvailability{}
|
||||
sac.cached = true
|
||||
return
|
||||
}
|
||||
|
||||
sac.serviceAvailability = serviceAvailability
|
||||
sac.cached = true
|
||||
|
||||
}
|
||||
|
||||
// CheckServiceAvailable validates if a service is available and provides user feedback
|
||||
func (sac *ServiceAvailabilityChecker) CheckServiceAvailable(serviceType models.ServiceType, actionDescription string) bool {
|
||||
if sac.skipAvailabilityCheck {
|
||||
return true
|
||||
}
|
||||
|
||||
sac.loadServiceAvailability()
|
||||
|
||||
// If we couldn't load availability data, allow the operation
|
||||
if sac.serviceAvailability == nil || sac.serviceAvailability.Services == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if sac.serviceAvailability.IsServiceAvailable(serviceType) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Service is not available - provide helpful feedback
|
||||
serviceName := formatServiceTypeForDisplay(serviceType)
|
||||
PrintError(fmt.Sprintf("Cannot %s: %s service is not available", actionDescription, serviceName))
|
||||
|
||||
// Get specific reason if available
|
||||
service := sac.serviceAvailability.GetServiceByType(serviceType)
|
||||
if service != nil && service.Reason != "" {
|
||||
PrintError(fmt.Sprintf("Reason: %s", service.Reason))
|
||||
}
|
||||
|
||||
// Provide troubleshooting hints
|
||||
sac.provideTroubleshootingHints(serviceType)
|
||||
|
||||
// Suggest alternatives
|
||||
sac.suggestAlternatives(serviceType, actionDescription)
|
||||
|
||||
PrintWarning("To bypass this check, set SOUNDTOUCH_SKIP_AVAILABILITY_CHECK=true")
|
||||
return false
|
||||
}
|
||||
|
||||
// CheckSourceAvailable validates if a source string corresponds to an available service
|
||||
func (sac *ServiceAvailabilityChecker) CheckSourceAvailable(source, actionDescription string) bool {
|
||||
if sac.skipAvailabilityCheck {
|
||||
return true
|
||||
}
|
||||
|
||||
serviceType := sourceToServiceType(source)
|
||||
if serviceType == "" {
|
||||
// Unknown source type, allow it (might be a valid source not in our list)
|
||||
return true
|
||||
}
|
||||
|
||||
return sac.CheckServiceAvailable(serviceType, actionDescription)
|
||||
}
|
||||
|
||||
// ValidateSpotifyAvailable checks Spotify availability for Spotify-specific operations
|
||||
func (sac *ServiceAvailabilityChecker) ValidateSpotifyAvailable(actionDescription string) bool {
|
||||
return sac.CheckServiceAvailable(models.ServiceTypeSpotify, actionDescription)
|
||||
}
|
||||
|
||||
// ValidateBluetoothAvailable checks Bluetooth availability for Bluetooth operations
|
||||
func (sac *ServiceAvailabilityChecker) ValidateBluetoothAvailable(actionDescription string) bool {
|
||||
return sac.CheckServiceAvailable(models.ServiceTypeBluetooth, actionDescription)
|
||||
}
|
||||
|
||||
// ValidateTuneInAvailable checks TuneIn availability for radio operations
|
||||
func (sac *ServiceAvailabilityChecker) ValidateTuneInAvailable(actionDescription string) bool {
|
||||
return sac.CheckServiceAvailable(models.ServiceTypeTuneIn, actionDescription)
|
||||
}
|
||||
|
||||
// ValidatePandoraAvailable checks Pandora availability for Pandora operations
|
||||
func (sac *ServiceAvailabilityChecker) ValidatePandoraAvailable(actionDescription string) bool {
|
||||
return sac.CheckServiceAvailable(models.ServiceTypePandora, actionDescription)
|
||||
}
|
||||
|
||||
// GetAvailableStreamingServices returns a list of available streaming services for user feedback
|
||||
func (sac *ServiceAvailabilityChecker) GetAvailableStreamingServices() []string {
|
||||
if sac.skipAvailabilityCheck {
|
||||
return []string{"All services (availability check disabled)"}
|
||||
}
|
||||
|
||||
sac.loadServiceAvailability()
|
||||
|
||||
if sac.serviceAvailability == nil || sac.serviceAvailability.Services == nil {
|
||||
return []string{"Unable to determine available services"}
|
||||
}
|
||||
|
||||
streamingServices := sac.serviceAvailability.GetStreamingServices()
|
||||
|
||||
var available []string
|
||||
|
||||
for _, service := range streamingServices {
|
||||
if service.IsAvailable {
|
||||
available = append(available, formatServiceTypeForDisplay(models.ServiceType(service.Type)))
|
||||
}
|
||||
}
|
||||
|
||||
if len(available) == 0 {
|
||||
return []string{"No streaming services currently available"}
|
||||
}
|
||||
|
||||
return available
|
||||
}
|
||||
|
||||
// GetAvailableLocalServices returns a list of available local input services
|
||||
func (sac *ServiceAvailabilityChecker) GetAvailableLocalServices() []string {
|
||||
if sac.skipAvailabilityCheck {
|
||||
return []string{"All services (availability check disabled)"}
|
||||
}
|
||||
|
||||
sac.loadServiceAvailability()
|
||||
|
||||
if sac.serviceAvailability == nil || sac.serviceAvailability.Services == nil {
|
||||
return []string{"Unable to determine available services"}
|
||||
}
|
||||
|
||||
localServices := sac.serviceAvailability.GetLocalServices()
|
||||
|
||||
var available []string
|
||||
|
||||
for _, service := range localServices {
|
||||
if service.IsAvailable {
|
||||
available = append(available, formatServiceTypeForDisplay(models.ServiceType(service.Type)))
|
||||
}
|
||||
}
|
||||
|
||||
if len(available) == 0 {
|
||||
return []string{"No local input services currently available"}
|
||||
}
|
||||
|
||||
return available
|
||||
}
|
||||
|
||||
// provideTroubleshootingHints provides specific troubleshooting advice based on service type
|
||||
func (sac *ServiceAvailabilityChecker) provideTroubleshootingHints(serviceType models.ServiceType) {
|
||||
switch serviceType {
|
||||
case models.ServiceTypeBluetooth:
|
||||
PrintWarning("💡 Bluetooth troubleshooting:")
|
||||
PrintWarning(" • Check if your device supports Bluetooth audio input")
|
||||
PrintWarning(" • Ensure Bluetooth is enabled on the SoundTouch device")
|
||||
PrintWarning(" • Try restarting the device")
|
||||
|
||||
case models.ServiceTypeSpotify:
|
||||
PrintWarning("💡 Spotify troubleshooting:")
|
||||
PrintWarning(" • Ensure you have a Spotify Premium account")
|
||||
PrintWarning(" • Check if you're logged in to Spotify on the device")
|
||||
PrintWarning(" • Verify your network connection")
|
||||
|
||||
case models.ServiceTypeAirPlay:
|
||||
PrintWarning("💡 AirPlay troubleshooting:")
|
||||
PrintWarning(" • Ensure your Apple device and SoundTouch are on the same network")
|
||||
PrintWarning(" • Check that AirPlay is enabled in device settings")
|
||||
PrintWarning(" • Verify network connectivity")
|
||||
|
||||
case models.ServiceTypeAlexa:
|
||||
PrintWarning("💡 Alexa troubleshooting:")
|
||||
PrintWarning(" • Check if Amazon Alexa is properly configured")
|
||||
PrintWarning(" • Ensure the device is connected to your Amazon account")
|
||||
PrintWarning(" • Verify internet connectivity")
|
||||
|
||||
case models.ServiceTypeTuneIn:
|
||||
PrintWarning("💡 TuneIn troubleshooting:")
|
||||
PrintWarning(" • Check internet connectivity")
|
||||
PrintWarning(" • Verify the device can access external streaming services")
|
||||
|
||||
case models.ServiceTypePandora:
|
||||
PrintWarning("💡 Pandora troubleshooting:")
|
||||
PrintWarning(" • Ensure you have a valid Pandora account")
|
||||
PrintWarning(" • Check if you're logged in to Pandora on the device")
|
||||
PrintWarning(" • Verify internet connectivity")
|
||||
}
|
||||
}
|
||||
|
||||
// suggestAlternatives suggests alternative services when the requested one is unavailable
|
||||
func (sac *ServiceAvailabilityChecker) suggestAlternatives(serviceType models.ServiceType, _ string) {
|
||||
if sac.serviceAvailability == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch serviceType {
|
||||
case models.ServiceTypeSpotify:
|
||||
if sac.serviceAvailability.HasTuneIn() {
|
||||
PrintWarning("💡 Alternative: TuneIn Radio is available for music streaming")
|
||||
}
|
||||
if sac.serviceAvailability.HasPandora() {
|
||||
PrintWarning("💡 Alternative: Pandora is available for music streaming")
|
||||
}
|
||||
|
||||
case models.ServiceTypeBluetooth:
|
||||
if sac.serviceAvailability.HasAirPlay() {
|
||||
PrintWarning("💡 Alternative: AirPlay is available for wireless audio")
|
||||
}
|
||||
if sac.serviceAvailability.HasLocalMusic() {
|
||||
PrintWarning("💡 Alternative: Local Music Library is available")
|
||||
}
|
||||
|
||||
case models.ServiceTypeTuneIn:
|
||||
if sac.serviceAvailability.HasSpotify() {
|
||||
PrintWarning("💡 Alternative: Spotify is available for music streaming")
|
||||
}
|
||||
if sac.serviceAvailability.HasPandora() {
|
||||
PrintWarning("💡 Alternative: Pandora is available for music streaming")
|
||||
}
|
||||
}
|
||||
|
||||
// Show all available streaming services as suggestions
|
||||
available := sac.GetAvailableStreamingServices()
|
||||
if len(available) > 0 && available[0] != "No streaming services currently available" {
|
||||
PrintWarning(fmt.Sprintf("💡 Available streaming services: %s", strings.Join(available, ", ")))
|
||||
}
|
||||
}
|
||||
|
||||
// sourceToServiceType maps source strings to service types
|
||||
func sourceToServiceType(source string) models.ServiceType {
|
||||
switch strings.ToUpper(source) {
|
||||
case "SPOTIFY":
|
||||
return models.ServiceTypeSpotify
|
||||
case "BLUETOOTH":
|
||||
return models.ServiceTypeBluetooth
|
||||
case "AIRPLAY":
|
||||
return models.ServiceTypeAirPlay
|
||||
case "ALEXA":
|
||||
return models.ServiceTypeAlexa
|
||||
case "AMAZON":
|
||||
return models.ServiceTypeAmazon
|
||||
case "PANDORA":
|
||||
return models.ServiceTypePandora
|
||||
case "TUNEIN":
|
||||
return models.ServiceTypeTuneIn
|
||||
case "DEEZER":
|
||||
return models.ServiceTypeDeezer
|
||||
case "IHEART", "IHEARTRADIO":
|
||||
return models.ServiceTypeIHeart
|
||||
case "LOCAL_INTERNET_RADIO":
|
||||
return models.ServiceTypeLocalInternetRadio
|
||||
case "LOCAL_MUSIC":
|
||||
return models.ServiceTypeLocalMusic
|
||||
case "BMX":
|
||||
return models.ServiceTypeBMX
|
||||
case "NOTIFICATION":
|
||||
return models.ServiceTypeNotification
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// formatServiceTypeForDisplay formats service types for user-friendly display
|
||||
func formatServiceTypeForDisplay(serviceType models.ServiceType) string {
|
||||
switch serviceType {
|
||||
case models.ServiceTypeSpotify:
|
||||
return "Spotify"
|
||||
case models.ServiceTypeBluetooth:
|
||||
return "Bluetooth"
|
||||
case models.ServiceTypeAirPlay:
|
||||
return "AirPlay"
|
||||
case models.ServiceTypeAlexa:
|
||||
return "Amazon Alexa"
|
||||
case models.ServiceTypeAmazon:
|
||||
return "Amazon Music"
|
||||
case models.ServiceTypePandora:
|
||||
return "Pandora"
|
||||
case models.ServiceTypeTuneIn:
|
||||
return "TuneIn Radio"
|
||||
case models.ServiceTypeDeezer:
|
||||
return "Deezer"
|
||||
case models.ServiceTypeIHeart:
|
||||
return "iHeartRadio"
|
||||
case models.ServiceTypeLocalInternetRadio:
|
||||
return "Internet Radio"
|
||||
case models.ServiceTypeLocalMusic:
|
||||
return "Local Music Library"
|
||||
case models.ServiceTypeBMX:
|
||||
return "BMX"
|
||||
case models.ServiceTypeNotification:
|
||||
return "Notifications"
|
||||
default:
|
||||
return string(serviceType)
|
||||
}
|
||||
}
|
||||
|
||||
// PrintServiceAvailabilitySummary prints a summary of available services
|
||||
func (sac *ServiceAvailabilityChecker) PrintServiceAvailabilitySummary() {
|
||||
if sac.skipAvailabilityCheck {
|
||||
PrintWarning("Service availability checking is disabled")
|
||||
return
|
||||
}
|
||||
|
||||
sac.loadServiceAvailability()
|
||||
|
||||
if sac.serviceAvailability == nil || sac.serviceAvailability.Services == nil {
|
||||
PrintWarning("Unable to determine service availability")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("📊 Service Availability Summary:\n")
|
||||
fmt.Printf(" Total services: %d\n", sac.serviceAvailability.GetServiceCount())
|
||||
fmt.Printf(" Available: %d\n", sac.serviceAvailability.GetAvailableServiceCount())
|
||||
fmt.Printf(" Unavailable: %d\n", sac.serviceAvailability.GetUnavailableServiceCount())
|
||||
|
||||
// Show quick status for popular services
|
||||
fmt.Printf(" Popular services:\n")
|
||||
popularChecks := []struct {
|
||||
check func() bool
|
||||
name string
|
||||
}{
|
||||
{sac.serviceAvailability.HasSpotify, "Spotify"},
|
||||
{sac.serviceAvailability.HasBluetooth, "Bluetooth"},
|
||||
{sac.serviceAvailability.HasAirPlay, "AirPlay"},
|
||||
{sac.serviceAvailability.HasTuneIn, "TuneIn Radio"},
|
||||
{sac.serviceAvailability.HasPandora, "Pandora"},
|
||||
}
|
||||
|
||||
for _, check := range popularChecks {
|
||||
status := "❌"
|
||||
if check.check() {
|
||||
status = "✅"
|
||||
}
|
||||
fmt.Printf(" %s %s\n", status, check.name)
|
||||
}
|
||||
|
||||
fmt.Printf("💡 Use 'soundtouch-cli sources list' to see configured sources\n")
|
||||
}
|
||||
Reference in New Issue
Block a user