package main import ( "fmt" "strings" bmxpkg "github.com/gesellix/bose-soundtouch/pkg/service/bmx" "github.com/gesellix/bose-soundtouch/pkg/models" "github.com/urfave/cli/v2" ) // storeCurrentPreset handles storing currently playing content as preset func storeCurrentPreset(c *cli.Context) error { slot := c.Int("slot") clientConfig := GetClientConfig(c) PrintDeviceHeader(fmt.Sprintf("Storing current content as preset %d", slot), clientConfig.Host, clientConfig.Port) client, err := CreateSoundTouchClient(clientConfig) if err != nil { PrintError(fmt.Sprintf("Failed to create client: %v", err)) return err } // Check what's currently playing nowPlaying, err := client.GetNowPlaying() if err != nil { PrintError(fmt.Sprintf("Failed to get current content: %v", err)) return err } if nowPlaying.IsEmpty() { PrintError("No content currently playing") return fmt.Errorf("no content currently playing") } if nowPlaying.ContentItem == nil { PrintError("Current content has no preset information") return fmt.Errorf("current content cannot be saved as preset") } if !nowPlaying.ContentItem.IsPresetable { PrintError("Current content cannot be saved as preset") fmt.Printf(" Content: %s\n", nowPlaying.Track) fmt.Printf(" Source: %s\n", nowPlaying.Source) return fmt.Errorf("current content cannot be preset") } // Show what we're about to store fmt.Printf("Current Content:\n") 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) } fmt.Printf(" Source: %s\n", nowPlaying.Source) if nowPlaying.ContentItem.Location != "" { fmt.Printf(" Location: %s\n", nowPlaying.ContentItem.Location) } // Store as preset err = client.StoreCurrentAsPreset(slot) if err != nil { PrintError(fmt.Sprintf("Failed to store preset: %v", err)) return err } PrintSuccess(fmt.Sprintf("Stored current content as preset %d", slot)) return nil } // presetParams holds parameters for storing a preset type presetParams struct { slot int source string location string sourceAccount string name string itemType string artwork string serviceURL string } // 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"), serviceURL: strings.TrimRight(c.String("service-url"), "/"), } } // isOrionLocation reports whether location is already an Orion station URL so // we don't double-wrap it. func isOrionLocation(location string) bool { return strings.Contains(location, "/core02/svc-bmx-adapter-orion/") } // resolveLocationAndMetadata resolves location and fetches metadata if needed func resolveLocationAndMetadata(params *presetParams) error { originalLocation := params.location resolvedSource, resolvedLocation := resolveLocation(params.source, params.location) params.source = resolvedSource params.location = resolvedLocation // For LOCAL_INTERNET_RADIO, the speaker's BMX module calls GET on the stored // location expecting a BmxPlaybackResponse JSON (the Orion station format). // A direct stream URL returns raw audio, which BMX cannot parse, so playback // silently stays on the previous source. if params.source == "LOCAL_INTERNET_RADIO" && !isOrionLocation(params.location) && (strings.HasPrefix(params.location, "http://") || strings.HasPrefix(params.location, "https://")) { if params.serviceURL != "" { params.location = bmxpkg.BuildOrionLocation(params.serviceURL, params.name, params.artwork, resolvedLocation) fmt.Printf(" Wrapped stream URL in Orion location for LOCAL_INTERNET_RADIO\n") } else { fmt.Printf(" ⚠️ --service-url not set: storing raw stream URL as location.\n") fmt.Printf(" The speaker's BMX module expects an Orion station URL, not raw audio.\n") fmt.Printf(" Re-run with --service-url to fix this.\n") } } // If metadata (name or artwork) is missing, try to fetch it if params.name == "" || params.artwork == "" { var ( metadata *Metadata err error ) if params.source == "TUNEIN" && strings.Contains(originalLocation, "tunein.com/radio/") { metadata, err = fetchTuneInMetadata(originalLocation) } else if params.source == "SPOTIFY" && strings.Contains(originalLocation, "open.spotify.com/") { metadata, err = fetchSpotifyMetadata(originalLocation) } if err == nil && metadata != nil { if params.name == "" { params.name = metadata.Name } if params.artwork == "" { params.artwork = metadata.Artwork } } } return nil } // validatePresetParams validates required preset parameters func validatePresetParams(params *presetParams) error { if params.source == "" { return fmt.Errorf("source is required (use --source)") } if params.location == "" { return fmt.Errorf("location is required (use --location)") } return nil } // createContentItem creates a ContentItem from preset parameters func createContentItem(params *presetParams) *models.ContentItem { contentItem := &models.ContentItem{ Source: params.source, Type: params.itemType, Location: params.location, SourceAccount: params.sourceAccount, IsPresetable: true, ItemName: params.name, ContainerArt: params.artwork, } // Set default type if not specified if params.itemType == "" { switch params.source { case "SPOTIFY": contentItem.Type = "uri" case "TUNEIN", "LOCAL_INTERNET_RADIO": contentItem.Type = "stationurl" default: contentItem.Type = "" } } return contentItem } // printPresetContent displays what content will be stored func printPresetContent(params *presetParams) { fmt.Printf("Content to store:\n") 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 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(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", params.slot)) return nil } // removePreset handles removing a preset func removePreset(c *cli.Context) error { slot := c.Int("slot") clientConfig := GetClientConfig(c) PrintDeviceHeader(fmt.Sprintf("Removing preset %d", slot), clientConfig.Host, clientConfig.Port) client, err := CreateSoundTouchClient(clientConfig) if err != nil { PrintError(fmt.Sprintf("Failed to create client: %v", err)) return err } // Check if preset exists first presets, err := client.GetPresets() if err != nil { PrintError(fmt.Sprintf("Failed to get presets: %v", err)) return err } preset := presets.GetPresetByID(slot) if preset == nil || preset.IsEmpty() { PrintError(fmt.Sprintf("Preset %d is already empty", slot)) return fmt.Errorf("preset %d does not exist", slot) } // Show what we're removing fmt.Printf("Removing preset %d:\n", slot) fmt.Printf(" Name: %s\n", preset.GetDisplayName()) fmt.Printf(" Source: %s\n", preset.GetSource()) // Remove preset err = client.RemovePreset(slot) if err != nil { PrintError(fmt.Sprintf("Failed to remove preset: %v", err)) return err } PrintSuccess(fmt.Sprintf("Removed preset %d", slot)) return nil } // selectPresetNew handles selecting a preset (new version that works with subcommands) func selectPresetNew(c *cli.Context) error { slot := c.Int("slot") clientConfig := GetClientConfig(c) PrintDeviceHeader(fmt.Sprintf("Selecting preset %d", slot), clientConfig.Host, clientConfig.Port) client, err := CreateSoundTouchClient(clientConfig) if err != nil { PrintError(fmt.Sprintf("Failed to create client: %v", err)) return err } err = client.SelectPreset(slot) if err != nil { PrintError(fmt.Sprintf("Failed to select preset: %v", err)) return err } PrintSuccess(fmt.Sprintf("Preset %d selected", slot)) return nil } // listPresets handles listing all presets (alias for existing getPresets command) func listPresets(c *cli.Context) error { return getPresets(c) }