refactor: reduce cyclomatic complexity for all high-complexity functions

Major refactoring to improve code maintainability and readability:

- printNavigationResults: Split into multiple helper functions (16->9)
- printSearchResults: Extract song/artist/station printing functions (18->8)
- compareSourcesAndAvailability: Separate comparison logic and summary (17->7)
- storePreset: Extract parameter validation and content creation (19->8)
- getNowPlaying: Break into focused helper functions (23->5)

Benefits:
- All cyclomatic complexity issues resolved (5->0)
- Improved code readability and maintainability
- Single responsibility principle applied to helper functions
- Easier testing and debugging of individual components
- Maintained all existing functionality

Resolves all gocyclo linting issues while preserving functionality.
This commit is contained in:
Tobias Gesellchen
2026-02-01 22:10:25 +01:00
parent b7c8067366
commit f8f80a5121
5 changed files with 323 additions and 183 deletions
+53 -32
View File
@@ -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 <location> --type <type>\n")
+67 -28
View File
@@ -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
+95 -52
View File
@@ -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
}
+45 -30
View File
@@ -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
+63 -41
View File
@@ -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 <token> --name <name>\n", response.Source)
if hasAccountResults(response) {