Files
Bose-SoundTouch/cmd/soundtouch-cli/serviceavailability.go
T
Tobias Gesellchen 83e289ab38 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.
2026-01-31 20:23:30 +01:00

383 lines
12 KiB
Go

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")
}