diff --git a/cmd/soundtouch-cli/cmd_navigation.go b/cmd/soundtouch-cli/cmd_navigation.go index 30745c84..f4aa4005 100644 --- a/cmd/soundtouch-cli/cmd_navigation.go +++ b/cmd/soundtouch-cli/cmd_navigation.go @@ -204,40 +204,61 @@ func printNavigationResults(response *models.NavigateResponse, title string) { fmt.Printf(" Items:\n") for i, item := range response.Items { - fmt.Printf(" %d. %s\n", i+1, item.GetDisplayName()) - - // Show type and source for identification - if item.ContentItem != nil { - if item.ContentItem.Source != "" && item.ContentItem.Source != response.Source { - fmt.Printf(" Source: %s\n", item.ContentItem.Source) - } - if item.Type != "" { - fmt.Printf(" Type: %s\n", item.Type) - } - if item.ContentItem.Location != "" && len(item.ContentItem.Location) < 100 { - fmt.Printf(" Location: %s\n", item.ContentItem.Location) - } - } - - // Show additional metadata - if item.ArtistName != "" { - fmt.Printf(" Artist: %s\n", item.ArtistName) - } - if item.AlbumName != "" { - fmt.Printf(" Album: %s\n", item.AlbumName) - } - - // Show if it's a container that can be browsed further - if item.IsDirectory() { - fmt.Printf(" 📁 Directory (can browse into)\n") - } else if item.IsPlayable() { - fmt.Printf(" ▶️ Playable content\n") - } - - fmt.Println() + printNavigationItem(item, i+1, response.Source) } - // Show navigation hints + printNavigationHints(response) +} + +// printNavigationItem prints a single navigation item with its metadata +func printNavigationItem(item models.NavigateItem, index int, responseSource string) { + fmt.Printf(" %d. %s\n", index, item.GetDisplayName()) + + printContentItemInfo(item, responseSource) + printItemMetadata(item) + printItemType(item) + + fmt.Println() +} + +// printContentItemInfo prints content item information (source, type, location) +func printContentItemInfo(item models.NavigateItem, responseSource string) { + if item.ContentItem == nil { + return + } + + if item.ContentItem.Source != "" && item.ContentItem.Source != responseSource { + fmt.Printf(" Source: %s\n", item.ContentItem.Source) + } + if item.Type != "" { + fmt.Printf(" Type: %s\n", item.Type) + } + if item.ContentItem.Location != "" && len(item.ContentItem.Location) < 100 { + fmt.Printf(" Location: %s\n", item.ContentItem.Location) + } +} + +// printItemMetadata prints additional metadata (artist, album) +func printItemMetadata(item models.NavigateItem) { + if item.ArtistName != "" { + fmt.Printf(" Artist: %s\n", item.ArtistName) + } + if item.AlbumName != "" { + fmt.Printf(" Album: %s\n", item.AlbumName) + } +} + +// printItemType prints whether the item is a directory or playable +func printItemType(item models.NavigateItem) { + if item.IsDirectory() { + fmt.Printf(" 📁 Directory (can browse into)\n") + } else if item.IsPlayable() { + fmt.Printf(" ▶️ Playable content\n") + } +} + +// printNavigationHints prints helpful navigation hints +func printNavigationHints(response *models.NavigateResponse) { directories := response.GetDirectories() if len(directories) > 0 { fmt.Printf(" 💡 To browse into a directory, use: browse container --location --type \n") diff --git a/cmd/soundtouch-cli/cmd_playback.go b/cmd/soundtouch-cli/cmd_playback.go index 86858e4d..22180e9b 100644 --- a/cmd/soundtouch-cli/cmd_playback.go +++ b/cmd/soundtouch-cli/cmd_playback.go @@ -32,61 +32,100 @@ func getNowPlaying(c *cli.Context) error { return nil } + printBasicPlaybackInfo(nowPlaying) + printTrackInfo(nowPlaying) + printTimeInfo(nowPlaying) + printStreamInfo(nowPlaying) + printContentDetails(nowPlaying, c.Bool("verbose")) + printPlaybackStatus(nowPlaying) + + return nil +} + +// printBasicPlaybackInfo prints basic source and status information +func printBasicPlaybackInfo(nowPlaying *models.NowPlaying) { fmt.Printf(" Source: %s\n", nowPlaying.Source) if nowPlaying.SourceAccount != "" { fmt.Printf(" Source Account: %s\n", nowPlaying.SourceAccount) } fmt.Printf(" Status: %s\n", nowPlaying.PlayStatus.String()) +} +// printTrackInfo prints track, artist, and album information +func printTrackInfo(nowPlaying *models.NowPlaying) { if nowPlaying.Track != "" { fmt.Printf(" Track: %s\n", nowPlaying.Track) } - if nowPlaying.Artist != "" { fmt.Printf(" Artist: %s\n", nowPlaying.Artist) } - if nowPlaying.Album != "" { fmt.Printf(" Album: %s\n", nowPlaying.Album) } +} - if nowPlaying.HasTimeInfo() { - fmt.Printf(" Duration: %s\n", nowPlaying.FormatDuration()) - - if nowPlaying.Position != nil { - fmt.Printf(" Position: %s\n", nowPlaying.FormatPosition()) - } +// printTimeInfo prints duration and position information +func printTimeInfo(nowPlaying *models.NowPlaying) { + if !nowPlaying.HasTimeInfo() { + return } + fmt.Printf(" Duration: %s\n", nowPlaying.FormatDuration()) + if nowPlaying.Position != nil { + fmt.Printf(" Position: %s\n", nowPlaying.FormatPosition()) + } +} + +// printStreamInfo prints stream type information +func printStreamInfo(nowPlaying *models.NowPlaying) { if nowPlaying.StreamType != "" { fmt.Printf(" Stream Type: %s\n", nowPlaying.StreamType) } +} - // Show ContentItem details if verbose flag is set or always show location if available - verbose := c.Bool("verbose") - showDetails := verbose || (nowPlaying.ContentItem != nil && nowPlaying.ContentItem.Location != "") - - if showDetails && nowPlaying.ContentItem != nil { - fmt.Printf("\nContent Details:\n") - if nowPlaying.ContentItem.Location != "" { - fmt.Printf(" Location: %s\n", nowPlaying.ContentItem.Location) - } - if verbose && nowPlaying.ContentItem.Type != "" { - fmt.Printf(" Content Type: %s\n", nowPlaying.ContentItem.Type) - } - if verbose && nowPlaying.ContentItem.ItemName != "" && nowPlaying.ContentItem.ItemName != nowPlaying.Track { - fmt.Printf(" Item Name: %s\n", nowPlaying.ContentItem.ItemName) - } - if verbose { - fmt.Printf(" Presetable: %t\n", nowPlaying.ContentItem.IsPresetable) - } +// printContentDetails prints detailed content information when verbose or location is available +func printContentDetails(nowPlaying *models.NowPlaying, verbose bool) { + if nowPlaying.ContentItem == nil { + return } + showDetails := verbose || nowPlaying.ContentItem.Location != "" + if !showDetails { + return + } + + fmt.Printf("\nContent Details:\n") + printContentLocation(nowPlaying.ContentItem) + printVerboseContentInfo(nowPlaying, verbose) +} + +// printContentLocation prints the content location +func printContentLocation(contentItem *models.ContentItem) { + if contentItem.Location != "" { + fmt.Printf(" Location: %s\n", contentItem.Location) + } +} + +// printVerboseContentInfo prints verbose content information +func printVerboseContentInfo(nowPlaying *models.NowPlaying, verbose bool) { + if !verbose || nowPlaying.ContentItem == nil { + return + } + + if nowPlaying.ContentItem.Type != "" { + fmt.Printf(" Content Type: %s\n", nowPlaying.ContentItem.Type) + } + if nowPlaying.ContentItem.ItemName != "" && nowPlaying.ContentItem.ItemName != nowPlaying.Track { + fmt.Printf(" Item Name: %s\n", nowPlaying.ContentItem.ItemName) + } + fmt.Printf(" Presetable: %t\n", nowPlaying.ContentItem.IsPresetable) +} + +// printPlaybackStatus prints special status messages +func printPlaybackStatus(nowPlaying *models.NowPlaying) { if nowPlaying.PlayStatus == models.PlayStatusBuffering { fmt.Printf("\nNote: Content is buffering\n") } - - return nil } // playCommand handles play command diff --git a/cmd/soundtouch-cli/cmd_preset.go b/cmd/soundtouch-cli/cmd_preset.go index 20398dea..d4ea4351 100644 --- a/cmd/soundtouch-cli/cmd_preset.go +++ b/cmd/soundtouch-cli/cmd_preset.go @@ -70,67 +70,78 @@ func storeCurrentPreset(c *cli.Context) error { return nil } -// storePreset handles storing specific content as preset -func storePreset(c *cli.Context) error { - slot := c.Int("slot") - source := c.String("source") - location := c.String("location") - sourceAccount := c.String("source-account") - name := c.String("name") - itemType := c.String("type") - artwork := c.String("artwork") +// presetParams holds parameters for storing a preset +type presetParams struct { + slot int + source string + location string + sourceAccount string + name string + itemType string + artwork string +} - // Resolve location and source from potential URLs - resolvedSource, resolvedLocation := resolveLocation(source, location) - if resolvedLocation != location && (source == "" || source == "TUNEIN") { +// extractPresetParams extracts parameters from CLI context +func extractPresetParams(c *cli.Context) *presetParams { + return &presetParams{ + slot: c.Int("slot"), + source: c.String("source"), + location: c.String("location"), + sourceAccount: c.String("source-account"), + name: c.String("name"), + itemType: c.String("type"), + artwork: c.String("artwork"), + } +} + +// resolveLocationAndMetadata resolves location and fetches metadata if needed +func resolveLocationAndMetadata(params *presetParams) error { + resolvedSource, resolvedLocation := resolveLocation(params.source, params.location) + if resolvedLocation != params.location && (params.source == "" || params.source == "TUNEIN") { // If location was a TuneIn URL, fetch metadata if name or artwork is missing - if name == "" || artwork == "" { - metadata, err := fetchTuneInMetadata(location) + if params.name == "" || params.artwork == "" { + metadata, err := fetchTuneInMetadata(params.location) if err == nil && metadata != nil { - if name == "" { - name = metadata.Name + if params.name == "" { + params.name = metadata.Name } - if artwork == "" { - artwork = metadata.Artwork + if params.artwork == "" { + params.artwork = metadata.Artwork } } } } - source = resolvedSource - location = resolvedLocation + params.source = resolvedSource + params.location = resolvedLocation + return nil +} - clientConfig := GetClientConfig(c) - - if source == "" { +// validatePresetParams validates required preset parameters +func validatePresetParams(params *presetParams) error { + if params.source == "" { return fmt.Errorf("source is required (use --source)") } - - if location == "" { + if params.location == "" { return fmt.Errorf("location is required (use --location)") } + return nil +} - PrintDeviceHeader(fmt.Sprintf("Storing %s content as preset %d", source, slot), clientConfig.Host, clientConfig.Port) - - client, err := CreateSoundTouchClient(clientConfig) - if err != nil { - PrintError(fmt.Sprintf("Failed to create client: %v", err)) - return err - } - - // Create content item +// createContentItem creates a ContentItem from preset parameters +func createContentItem(params *presetParams) *models.ContentItem { contentItem := &models.ContentItem{ - Source: source, - Type: itemType, - Location: location, - SourceAccount: sourceAccount, + Source: params.source, + Type: params.itemType, + Location: params.location, + SourceAccount: params.sourceAccount, IsPresetable: true, - ItemName: name, - ContainerArt: artwork, + ItemName: params.name, + ContainerArt: params.artwork, } // Set default type if not specified - if itemType == "" { - switch source { + if params.itemType == "" { + switch params.source { case "SPOTIFY": contentItem.Type = "uri" case "TUNEIN", "LOCAL_INTERNET_RADIO": @@ -140,26 +151,58 @@ func storePreset(c *cli.Context) error { } } - // Show what we're storing + return contentItem +} + +// printPresetContent displays what content will be stored +func printPresetContent(params *presetParams) { fmt.Printf("Content to store:\n") - fmt.Printf(" Name: %s\n", name) - fmt.Printf(" Source: %s\n", source) - fmt.Printf(" Location: %s\n", location) - if sourceAccount != "" { - fmt.Printf(" Source Account: %s\n", sourceAccount) + fmt.Printf(" Name: %s\n", params.name) + fmt.Printf(" Source: %s\n", params.source) + fmt.Printf(" Location: %s\n", params.location) + if params.sourceAccount != "" { + fmt.Printf(" Source Account: %s\n", params.sourceAccount) } - if itemType != "" { - fmt.Printf(" Type: %s\n", itemType) + if params.itemType != "" { + fmt.Printf(" Type: %s\n", params.itemType) + } +} + +// storePreset handles storing specific content as preset +func storePreset(c *cli.Context) error { + // Extract parameters + params := extractPresetParams(c) + + // Resolve location and fetch metadata if needed + if err := resolveLocationAndMetadata(params); err != nil { + return err } + // Validate required parameters + if err := validatePresetParams(params); err != nil { + return err + } + + clientConfig := GetClientConfig(c) + PrintDeviceHeader(fmt.Sprintf("Storing %s content as preset %d", params.source, params.slot), clientConfig.Host, clientConfig.Port) + + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + PrintError(fmt.Sprintf("Failed to create client: %v", err)) + return err + } + + contentItem := createContentItem(params) + printPresetContent(params) + // Store preset - err = client.StorePreset(slot, contentItem) + err = client.StorePreset(params.slot, contentItem) if err != nil { PrintError(fmt.Sprintf("Failed to store preset: %v", err)) return err } - PrintSuccess(fmt.Sprintf("Stored content as preset %d", slot)) + PrintSuccess(fmt.Sprintf("Stored content as preset %d", params.slot)) return nil } diff --git a/cmd/soundtouch-cli/cmd_source.go b/cmd/soundtouch-cli/cmd_source.go index 60af89ed..6ed1bd59 100644 --- a/cmd/soundtouch-cli/cmd_source.go +++ b/cmd/soundtouch-cli/cmd_source.go @@ -309,6 +309,14 @@ func compareSourcesAndAvailability(c *cli.Context) error { fmt.Printf("Source vs Availability Comparison:\n\n") + performSourceComparisons(sources, serviceAvailability) + printSourceSummary(sources, serviceAvailability) + + return nil +} + +// performSourceComparisons compares configured sources with availability +func performSourceComparisons(sources *models.Sources, serviceAvailability *models.ServiceAvailability) { // Check key services comparisons := []struct { name string @@ -331,40 +339,48 @@ func compareSourcesAndAvailability(c *cli.Context) error { } for _, comp := range comparisons { - configured := comp.configuredCheck() - available := comp.availableCheck() + compareServiceStatus(comp.name, comp.configuredCheck(), comp.availableCheck(), serviceAvailability) + } +} - fmt.Printf("🔍 %s:\n", comp.name) - fmt.Printf(" Configured: %s\n", boolToStatus(configured)) - fmt.Printf(" Available: %s\n", boolToStatus(available)) +// compareServiceStatus compares a single service's configuration vs availability +func compareServiceStatus(serviceName string, configured, available bool, serviceAvailability *models.ServiceAvailability) { + fmt.Printf("🔍 %s:\n", serviceName) + 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) + switch { + case available && !configured: + fmt.Printf(" 💡 %s is available but not configured - consider setting it up\n", serviceName) + case configured && !available: + fmt.Printf(" ⚠️ %s is configured but not available - check device status\n", serviceName) + printServiceUnavailableReason(serviceName, serviceAvailability) + case configured && available: + fmt.Printf(" ✅ %s is properly configured and available\n", serviceName) + default: + fmt.Printf(" ➖ %s is neither configured nor available\n", serviceName) + } + fmt.Println() +} - // 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() +// printServiceUnavailableReason prints the reason why a service is unavailable +func printServiceUnavailableReason(serviceName string, serviceAvailability *models.ServiceAvailability) { + var service *models.Service + + switch serviceName { + case "Spotify": + service = serviceAvailability.GetServiceByType(models.ServiceTypeSpotify) + case "Bluetooth": + service = serviceAvailability.GetServiceByType(models.ServiceTypeBluetooth) } + if service != nil && service.Reason != "" { + fmt.Printf(" 📝 Reason: %s\n", service.Reason) + } +} + +// printSourceSummary prints a summary of sources and services +func printSourceSummary(sources *models.Sources, serviceAvailability *models.ServiceAvailability) { // Summary fmt.Printf("📊 Summary:\n") fmt.Printf(" Total configured sources: %d\n", sources.GetSourceCount()) @@ -372,7 +388,6 @@ func compareSourcesAndAvailability(c *cli.Context) error { 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 diff --git a/cmd/soundtouch-cli/cmd_station.go b/cmd/soundtouch-cli/cmd_station.go index 21ba80b5..1301dba6 100644 --- a/cmd/soundtouch-cli/cmd_station.go +++ b/cmd/soundtouch-cli/cmd_station.go @@ -269,55 +269,77 @@ func printSearchResults(response *models.SearchStationResponse, searchTerm strin artists := response.GetArtists() stations := response.GetStations() - if len(songs) > 0 { - fmt.Printf("\n 🎵 Songs (%d):\n", len(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) - } - if song.Album != "" { - fmt.Printf(" Album: %s\n", song.Album) - } - if song.SourceAccount != "" { - fmt.Printf(" Account: %s\n", song.SourceAccount) - } - fmt.Printf(" Token: %s\n", song.Token) - fmt.Println() - } + printSongs(songs) + printArtists(artists) + printStations(stations) + printSearchHints(response, songs, artists, stations) +} + +// printSongs prints song search results +func printSongs(songs []models.SearchResult) { + if len(songs) == 0 { + return } - if len(artists) > 0 { - fmt.Printf(" 🎤 Artists (%d):\n", len(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) - } - fmt.Printf(" Token: %s\n", artist.Token) - fmt.Println() + fmt.Printf("\n 🎵 Songs (%d):\n", len(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) } + if song.Album != "" { + fmt.Printf(" Album: %s\n", song.Album) + } + if song.SourceAccount != "" { + fmt.Printf(" Account: %s\n", song.SourceAccount) + } + fmt.Printf(" Token: %s\n", song.Token) + fmt.Println() + } +} + +// printArtists prints artist search results +func printArtists(artists []models.SearchResult) { + if len(artists) == 0 { + return } - if len(stations) > 0 { - fmt.Printf(" 📻 Stations (%d):\n", len(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) - } - fmt.Printf(" Token: %s\n", station.Token) - if station.Description != "" { - fmt.Printf(" Description: %s\n", station.Description) - } - fmt.Println() + fmt.Printf(" 🎤 Artists (%d):\n", len(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) } + fmt.Printf(" Token: %s\n", artist.Token) + fmt.Println() + } +} + +// printStations prints station search results +func printStations(stations []models.SearchResult) { + if len(stations) == 0 { + return } - // Show usage hints + fmt.Printf(" 📻 Stations (%d):\n", len(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) + } + fmt.Printf(" Token: %s\n", station.Token) + if station.Description != "" { + fmt.Printf(" Description: %s\n", station.Description) + } + fmt.Println() + } +} + +// printSearchHints prints usage hints for search results +func printSearchHints(response *models.SearchStationResponse, songs, artists, stations []models.SearchResult) { fmt.Printf("💡 Usage hints:\n") fmt.Printf(" • To add a station and play it: station add --source %s --token --name \n", response.Source) if hasAccountResults(response) {