mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 00:56:16 +00:00
✨ 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.
383 lines
12 KiB
Go
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")
|
|
}
|