mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 16:46:17 +00:00
This commit completes the music service account management implementation and resolves all golangci-lint issues across the codebase. Music Service Account Management: • Add/remove accounts for all major streaming services (Spotify, Pandora, Amazon Music, Deezer, iHeartRadio) • Support for network music libraries (NAS/UPnP/DLNA servers) • Generic account management with service-specific convenience methods • Full CLI integration with 14 account management commands • Comprehensive test coverage with mock HTTP servers • Complete API documentation and usage examples New CLI Commands: • account list - List configured accounts • account add/remove - Generic account management • account add-spotify/remove-spotify - Spotify Premium • account add-pandora/remove-pandora - Pandora Music Service • account add-amazon/remove-amazon - Amazon Music • account add-deezer/remove-deezer - Deezer Premium • account add-iheart/remove-iheart - iHeartRadio • account add-nas/remove-nas - Network music libraries New API Methods: • SetMusicServiceAccount() / RemoveMusicServiceAccount() - Generic methods • AddSpotifyAccount() / RemoveSpotifyAccount() - Convenience methods • AddPandoraAccount() / RemovePandoraAccount() - Convenience methods • AddAmazonMusicAccount() / RemoveAmazonMusicAccount() - Convenience methods • AddDeezerAccount() / RemoveDeezerAccount() - Convenience methods • AddIHeartRadioAccount() / RemoveIHeartRadioAccount() - Convenience methods • AddStoredMusicAccount() / RemoveStoredMusicAccount() - Network libraries golangci-lint Fixes (36 issues resolved): • errcheck (3): Fixed unchecked w.Write() returns in tests • gocritic (3): Rewrote if-else chains to switch statements • gocyclo (6): Reduced cyclomatic complexity via helper function extraction • govet (12): Removed unused test data and field assignments • revive (6): Added package comments and fixed unused parameters • staticcheck (2): Replaced deprecated strings.Title usage • thelper (6): Added t.Helper() calls to test helper functions • unused (1): Removed unused createTestApp() function • whitespace/wsl_v5 (7): Fixed whitespace and formatting issues Code Quality Improvements: • All functions now have complexity < 15 (down from max 28) • Consistent error handling and validation patterns • Better separation of concerns with extracted helper functions • Zero external dependencies added for simple fixes • Comprehensive documentation with usage examples • Full backward compatibility maintained Files Added: • pkg/models/account.go - Account management models • pkg/models/account_test.go - Account model tests • pkg/client/account_test.go - Account client tests • cmd/soundtouch-cli/cmd_account.go - Account CLI commands • examples/account-management/ - Complete usage example • Updated docs/CLI-REFERENCE.md with account management section The implementation provides a complete, production-ready music service account management system with full CLI and programmatic API support.
358 lines
9.8 KiB
Go
358 lines
9.8 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gesellix/bose-soundtouch/pkg/models"
|
|
"github.com/urfave/cli/v2"
|
|
)
|
|
|
|
// introspectService handles getting introspect data for a specific service
|
|
func introspectService(c *cli.Context) error {
|
|
clientConfig := GetClientConfig(c)
|
|
|
|
client, err := CreateSoundTouchClient(clientConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
source := strings.ToUpper(c.String("source"))
|
|
sourceAccount := c.String("account")
|
|
|
|
// Check service availability first
|
|
checker := NewServiceAvailabilityChecker(client)
|
|
if !checker.CheckSourceAvailable(source, fmt.Sprintf("get introspect data for %s", strings.ToLower(source))) {
|
|
PrintWarning(fmt.Sprintf("Service %s may not be available, but continuing with introspect request...", source))
|
|
}
|
|
|
|
PrintDeviceHeader(fmt.Sprintf("Getting introspect data for %s", source), clientConfig.Host, clientConfig.Port)
|
|
|
|
if sourceAccount != "" {
|
|
fmt.Printf("Source Account: %s\n", sourceAccount)
|
|
}
|
|
|
|
fmt.Println()
|
|
|
|
response, err := client.Introspect(source, sourceAccount)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get introspect data: %w", err)
|
|
}
|
|
|
|
// Print basic information
|
|
fmt.Printf("=== %s Service Introspect Data ===\n", source)
|
|
printIntrospectBasicInfo(response)
|
|
|
|
// Print service state
|
|
fmt.Printf("\n=== Service State ===\n")
|
|
printIntrospectServiceState(response)
|
|
|
|
// Print capabilities
|
|
fmt.Printf("\n=== Service Capabilities ===\n")
|
|
printIntrospectCapabilities(response)
|
|
|
|
// Print history information
|
|
if response.GetMaxHistorySize() > 0 {
|
|
fmt.Printf("\n=== Content History ===\n")
|
|
printIntrospectHistory(response)
|
|
}
|
|
|
|
// Print technical details
|
|
if response.TokenLastChangedTimeSeconds > 0 || response.PlayStatusState != "" {
|
|
fmt.Printf("\n=== Technical Details ===\n")
|
|
printIntrospectTechnicalDetails(response)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// introspectSpotify handles getting Spotify introspect data using convenience method
|
|
func introspectSpotify(c *cli.Context) error {
|
|
clientConfig := GetClientConfig(c)
|
|
|
|
client, err := CreateSoundTouchClient(clientConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sourceAccount := c.String("account")
|
|
|
|
// Check Spotify availability
|
|
checker := NewServiceAvailabilityChecker(client)
|
|
if !checker.ValidateSpotifyAvailable("get Spotify introspect data") {
|
|
PrintWarning("Spotify may not be available, but continuing with introspect request...")
|
|
}
|
|
|
|
PrintDeviceHeader("Getting Spotify introspect data", clientConfig.Host, clientConfig.Port)
|
|
|
|
if sourceAccount != "" {
|
|
fmt.Printf("Spotify Account: %s\n", sourceAccount)
|
|
}
|
|
|
|
fmt.Println()
|
|
|
|
response, err := client.IntrospectSpotify(sourceAccount)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get Spotify introspect data: %w", err)
|
|
}
|
|
|
|
// Print Spotify-specific information
|
|
fmt.Printf("=== Spotify Service Introspect Data ===\n")
|
|
printIntrospectBasicInfo(response)
|
|
|
|
// Print service state with Spotify context
|
|
fmt.Printf("\n=== Spotify Service State ===\n")
|
|
printIntrospectServiceState(response)
|
|
|
|
// Print Spotify capabilities
|
|
fmt.Printf("\n=== Spotify Service Capabilities ===\n")
|
|
printIntrospectCapabilities(response)
|
|
|
|
// Show Spotify-specific recommendations
|
|
if response.IsInactive() {
|
|
fmt.Printf("\n💡 Spotify Setup Recommendations:\n")
|
|
|
|
if !response.HasUser() {
|
|
fmt.Printf(" • Sign in to your Spotify account on the device\n")
|
|
}
|
|
|
|
fmt.Printf(" • Use 'soundtouch-cli source select --source SPOTIFY' to activate Spotify\n")
|
|
fmt.Printf(" • Ensure you have Spotify Premium for full functionality\n")
|
|
}
|
|
|
|
// Print history information
|
|
if response.GetMaxHistorySize() > 0 {
|
|
fmt.Printf("\n=== Spotify Content History ===\n")
|
|
printIntrospectHistory(response)
|
|
}
|
|
|
|
// Print technical details
|
|
if response.TokenLastChangedTimeSeconds > 0 || response.PlayStatusState != "" {
|
|
fmt.Printf("\n=== Technical Details ===\n")
|
|
printIntrospectTechnicalDetails(response)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// introspectAllServices handles getting introspect data for all available services
|
|
func introspectAllServices(c *cli.Context) error {
|
|
clientConfig := GetClientConfig(c)
|
|
|
|
client, err := CreateSoundTouchClient(clientConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
PrintDeviceHeader("Getting introspect data for all services", clientConfig.Host, clientConfig.Port)
|
|
|
|
// Get service availability to know which services to check
|
|
serviceAvailability, err := client.GetServiceAvailability()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get service availability: %w", err)
|
|
}
|
|
|
|
// Services to introspect (only streaming services that support introspect)
|
|
servicesToCheck := []string{"SPOTIFY", "PANDORA", "TUNEIN", "AMAZON", "DEEZER"}
|
|
|
|
successCount := 0
|
|
failCount := 0
|
|
|
|
for i, source := range servicesToCheck {
|
|
if i > 0 {
|
|
fmt.Println("\n" + strings.Repeat("─", 50))
|
|
}
|
|
|
|
// Check if service is available
|
|
serviceType := sourceToServiceType(source)
|
|
if serviceType != "" && !serviceAvailability.IsServiceAvailable(serviceType) {
|
|
fmt.Printf("\n❌ %s: Service not available on this device\n", source)
|
|
continue
|
|
}
|
|
|
|
fmt.Printf("\n🔍 Getting introspect data for %s...\n", source)
|
|
|
|
response, err := client.Introspect(source, "")
|
|
if err != nil {
|
|
fmt.Printf("❌ %s: Failed to get introspect data - %v\n", source, err)
|
|
|
|
failCount++
|
|
|
|
continue
|
|
}
|
|
|
|
fmt.Printf("✅ %s: Successfully retrieved introspect data\n", source)
|
|
printIntrospectSummary(source, response)
|
|
|
|
successCount++
|
|
}
|
|
|
|
// Print summary
|
|
fmt.Print("\n" + strings.Repeat("═", 50) + "\n")
|
|
fmt.Printf("📊 Introspect Summary:\n")
|
|
fmt.Printf(" ✅ Successful: %d services\n", successCount)
|
|
fmt.Printf(" ❌ Failed: %d services\n", failCount)
|
|
fmt.Printf(" 📡 Total checked: %d services\n", len(servicesToCheck))
|
|
|
|
if successCount > 0 {
|
|
PrintSuccess(fmt.Sprintf("Successfully retrieved introspect data for %d services", successCount))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// printIntrospectBasicInfo prints basic introspect information
|
|
func printIntrospectBasicInfo(response *models.IntrospectResponse) {
|
|
fmt.Printf("State: %s\n", response.State)
|
|
|
|
if response.HasUser() {
|
|
fmt.Printf("User: %s\n", response.User)
|
|
}
|
|
|
|
fmt.Printf("Currently Playing: %s\n", formatBooleanStatus(response.IsPlaying))
|
|
|
|
if response.HasCurrentContent() {
|
|
fmt.Printf("Current Content: %s\n", response.CurrentURI)
|
|
}
|
|
|
|
fmt.Printf("Shuffle Mode: %s\n", response.ShuffleMode)
|
|
|
|
if response.HasSubscription() {
|
|
fmt.Printf("Subscription Type: %s\n", response.SubscriptionType)
|
|
}
|
|
}
|
|
|
|
// printIntrospectServiceState prints service state information
|
|
func printIntrospectServiceState(response *models.IntrospectResponse) {
|
|
if response.IsActive() {
|
|
fmt.Printf("✅ Service is ACTIVE\n")
|
|
} else if response.IsInactive() {
|
|
fmt.Printf("❌ Service is INACTIVE")
|
|
|
|
if response.GetState() == models.IntrospectStateInactiveUnselected {
|
|
fmt.Printf(" (Never been used)")
|
|
}
|
|
|
|
fmt.Println()
|
|
}
|
|
|
|
// Additional state information
|
|
if response.IsPlaying {
|
|
fmt.Printf("🎵 Currently playing content\n")
|
|
} else {
|
|
fmt.Printf("⏸️ Not currently playing\n")
|
|
}
|
|
|
|
if response.IsShuffleEnabled() {
|
|
fmt.Printf("🔀 Shuffle mode is ON\n")
|
|
} else {
|
|
fmt.Printf("➡️ Shuffle mode is OFF\n")
|
|
}
|
|
}
|
|
|
|
// printIntrospectCapabilities prints service capabilities
|
|
func printIntrospectCapabilities(response *models.IntrospectResponse) {
|
|
capabilities := []struct {
|
|
supported bool
|
|
feature string
|
|
icon string
|
|
}{
|
|
{response.SupportsSkipPrevious(), "Skip Previous", "⏮️"},
|
|
{response.SupportsSeek(), "Seek within tracks", "🎯"},
|
|
{response.SupportsResume(), "Resume playback", "▶️"},
|
|
}
|
|
|
|
for _, cap := range capabilities {
|
|
status := "❌"
|
|
if cap.supported {
|
|
status = "✅"
|
|
}
|
|
|
|
fmt.Printf("%s %s %s\n", status, cap.icon, cap.feature)
|
|
}
|
|
|
|
// Data collection status
|
|
if response.CollectsData() {
|
|
fmt.Printf("📊 Data collection: ENABLED\n")
|
|
} else {
|
|
fmt.Printf("🚫 Data collection: DISABLED\n")
|
|
}
|
|
}
|
|
|
|
// printIntrospectHistory prints content history information
|
|
func printIntrospectHistory(response *models.IntrospectResponse) {
|
|
fmt.Printf("Max History Size: %d items\n", response.GetMaxHistorySize())
|
|
}
|
|
|
|
// printIntrospectTechnicalDetails prints technical details
|
|
func printIntrospectTechnicalDetails(response *models.IntrospectResponse) {
|
|
if response.TokenLastChangedTimeSeconds > 0 {
|
|
// Convert timestamp to readable format
|
|
tokenTime := time.Unix(response.TokenLastChangedTimeSeconds, 0)
|
|
fmt.Printf("Token Last Changed: %s\n", tokenTime.Format("2006-01-02 15:04:05 MST"))
|
|
fmt.Printf("Token Timestamp: %d seconds since Unix epoch\n", response.TokenLastChangedTimeSeconds)
|
|
|
|
if response.TokenLastChangedTimeMicroseconds > 0 {
|
|
fmt.Printf("Token Microseconds: %d\n", response.TokenLastChangedTimeMicroseconds)
|
|
}
|
|
}
|
|
|
|
if response.PlayStatusState != "" {
|
|
fmt.Printf("Play Status State: %s\n", response.PlayStatusState)
|
|
}
|
|
|
|
fmt.Printf("Received Playback Request: %s\n", formatBooleanStatus(response.ReceivedPlaybackRequest))
|
|
}
|
|
|
|
// printIntrospectSummary prints a brief summary for the "all" command
|
|
func printIntrospectSummary(_ string, response *models.IntrospectResponse) {
|
|
fmt.Printf(" State: %s", response.State)
|
|
|
|
if response.HasUser() {
|
|
fmt.Printf(" (User: %s)", response.User)
|
|
}
|
|
|
|
fmt.Println()
|
|
|
|
fmt.Printf(" Playing: %s", formatBooleanStatus(response.IsPlaying))
|
|
|
|
if response.HasCurrentContent() {
|
|
fmt.Printf(" | Content: %.50s", response.CurrentURI)
|
|
|
|
if len(response.CurrentURI) > 50 {
|
|
fmt.Printf("...")
|
|
}
|
|
}
|
|
|
|
fmt.Println()
|
|
|
|
var capabilities []string
|
|
if response.SupportsSkipPrevious() {
|
|
capabilities = append(capabilities, "Skip")
|
|
}
|
|
|
|
if response.SupportsSeek() {
|
|
capabilities = append(capabilities, "Seek")
|
|
}
|
|
|
|
if response.SupportsResume() {
|
|
capabilities = append(capabilities, "Resume")
|
|
}
|
|
|
|
if len(capabilities) > 0 {
|
|
fmt.Printf(" Capabilities: %s\n", strings.Join(capabilities, ", "))
|
|
} else {
|
|
fmt.Printf(" Capabilities: None\n")
|
|
}
|
|
}
|
|
|
|
// formatBooleanStatus formats boolean values for display
|
|
func formatBooleanStatus(value bool) string {
|
|
if value {
|
|
return "✅ Yes"
|
|
}
|
|
|
|
return "❌ No"
|
|
}
|