package models import ( "encoding/xml" "strconv" "time" ) // Presets represents the response from /presets endpoint // Note: POST /presets is officially not supported by the SoundTouch API. // Presets can only be read, not created or modified via the API. type Presets struct { XMLName xml.Name `xml:"presets"` Preset []Preset `xml:"preset"` } // Preset represents an individual preset // Presets are read-only via the API and can only be created/modified // through the SoundTouch app or physical device controls. type Preset struct { XMLName xml.Name `xml:"preset"` ID int `xml:"id,attr"` CreatedOn *int64 `xml:"createdOn,attr,omitempty"` UpdatedOn *int64 `xml:"updatedOn,attr,omitempty"` ContentItem *ContentItem `xml:"ContentItem,omitempty"` } // GetCreatedTime returns the creation time as a time.Time func (p *Preset) GetCreatedTime() time.Time { if p.CreatedOn != nil { return time.Unix(*p.CreatedOn, 0) } return time.Time{} } // GetUpdatedTime returns the last updated time as a time.Time func (p *Preset) GetUpdatedTime() time.Time { if p.UpdatedOn != nil { return time.Unix(*p.UpdatedOn, 0) } return time.Time{} } // HasTimestamps returns true if the preset has creation/update timestamps func (p *Preset) HasTimestamps() bool { return p.CreatedOn != nil || p.UpdatedOn != nil } // GetDisplayName returns the best available display name for the preset func (p *Preset) GetDisplayName() string { if p.ContentItem != nil && p.ContentItem.ItemName != "" { return p.ContentItem.ItemName } return "Preset " + strconv.Itoa(p.ID) } // GetArtworkURL returns the artwork URL if available func (p *Preset) GetArtworkURL() string { if p.ContentItem != nil && p.ContentItem.ContainerArt != "" { return p.ContentItem.ContainerArt } return "" } // IsSpotifyPreset returns true if this is a Spotify preset func (p *Preset) IsSpotifyPreset() bool { return p.ContentItem != nil && p.ContentItem.Source == "SPOTIFY" } // IsEmpty returns true if the preset has no playable content. Two // placeholder shapes are observed in the wild and both count as empty: // // - (or ) — no ContentItem child at all. // Emitted by some firmware after a factory reset (issue #308). // - // — a placeholder ContentItem the firmware uses for unconfigured // slots, observed on FW 27.0.6 even on devices that were never // reset. // // Treating both as empty keeps GetEmptyPresetSlots, GetUsedPresetSlots // and HasPresets honest, and lets callers safely skip placeholders // before formatting a preset for display. func (p *Preset) IsEmpty() bool { if p.ContentItem == nil { return true } return p.ContentItem.Source == "" || p.ContentItem.Source == "INVALID_SOURCE" } // GetSource returns the source of the preset content func (p *Preset) GetSource() string { if p.ContentItem != nil { return p.ContentItem.Source } return "" } // GetSourceAccount returns the source account of the preset content func (p *Preset) GetSourceAccount() string { if p.ContentItem != nil { return p.ContentItem.SourceAccount } return "" } // GetContentType returns the content type of the preset func (p *Preset) GetContentType() string { if p.ContentItem != nil { return p.ContentItem.Type } return "" } // GetLocation returns the content location/URL func (p *Preset) GetLocation() string { if p.ContentItem != nil { return p.ContentItem.Location } return "" } // IsPresetable returns true if the content can be saved as a preset func (p *Preset) IsPresetable() bool { return p.ContentItem != nil && p.ContentItem.IsPresetable } // GetPresetCount returns the total number of presets func (ps *Presets) GetPresetCount() int { return len(ps.Preset) } // GetPresetByID returns a preset by its ID func (ps *Presets) GetPresetByID(id int) *Preset { for _, preset := range ps.Preset { if preset.ID == id { return &preset } } return nil } // GetSpotifyPresets returns all Spotify presets func (ps *Presets) GetSpotifyPresets() []Preset { var spotify []Preset for _, preset := range ps.Preset { if preset.IsSpotifyPreset() { spotify = append(spotify, preset) } } return spotify } // GetPresetsBySource returns presets filtered by source func (ps *Presets) GetPresetsBySource(source string) []Preset { var filtered []Preset for _, preset := range ps.Preset { if preset.GetSource() == source { filtered = append(filtered, preset) } } return filtered } // GetEmptyPresetSlots returns preset IDs that are empty (1-6) func (ps *Presets) GetEmptyPresetSlots() []int { var empty []int used := make(map[int]bool) // Mark used slots for _, preset := range ps.Preset { if !preset.IsEmpty() { used[preset.ID] = true } } // Find empty slots (1-6 are typical preset slots) for i := 1; i <= 6; i++ { if !used[i] { empty = append(empty, i) } } return empty } // HasPresets returns true if there are any presets configured func (ps *Presets) HasPresets() bool { for _, preset := range ps.Preset { if !preset.IsEmpty() { return true } } return false } // GetUsedPresetSlots returns preset IDs that have content func (ps *Presets) GetUsedPresetSlots() []int { var used []int for _, preset := range ps.Preset { if !preset.IsEmpty() { used = append(used, preset.ID) } } return used } // GetMostRecentPreset returns the most recently updated preset func (ps *Presets) GetMostRecentPreset() *Preset { var ( mostRecent *Preset latestTime int64 ) for _, preset := range ps.Preset { if preset.UpdatedOn != nil && *preset.UpdatedOn > latestTime { latestTime = *preset.UpdatedOn mostRecent = &preset } else if preset.CreatedOn != nil && preset.UpdatedOn == nil && *preset.CreatedOn > latestTime { latestTime = *preset.CreatedOn mostRecent = &preset } } return mostRecent } // GetOldestPreset returns the oldest preset func (ps *Presets) GetOldestPreset() *Preset { var oldest *Preset var earliestTime int64 = 9223372036854775807 // max int64 for _, preset := range ps.Preset { if preset.CreatedOn != nil && *preset.CreatedOn < earliestTime { earliestTime = *preset.CreatedOn oldest = &preset } } return oldest } // GetPresetsSummary returns a summary of preset usage func (ps *Presets) GetPresetsSummary() map[string]int { summary := map[string]int{ "total": ps.GetPresetCount(), "used": len(ps.GetUsedPresetSlots()), "empty": len(ps.GetEmptyPresetSlots()), "spotify": len(ps.GetSpotifyPresets()), } // Count by source sources := make(map[string]int) for _, preset := range ps.Preset { if !preset.IsEmpty() { source := preset.GetSource() sources[source]++ } } // Add source counts to summary for source, count := range sources { summary[source] = count } return summary }