package models import ( "encoding/xml" "strings" ) // Sources represents the response from /sources endpoint type Sources struct { XMLName xml.Name `xml:"sources"` DeviceID string `xml:"deviceID,attr"` SourceItem []SourceItem `xml:"sourceItem"` } // SourceItem represents an individual audio source type SourceItem struct { Source string `xml:"source,attr"` SourceAccount string `xml:"sourceAccount,attr,omitempty"` Status SourceStatus `xml:"status,attr"` IsLocal bool `xml:"isLocal,attr"` MultiroomAllowed bool `xml:"multiroomallowed,attr"` DisplayName string `xml:",chardata"` } // SourceStatus represents the availability status of a source type SourceStatus string const ( // SourceStatusReady indicates the source is ready for use SourceStatusReady SourceStatus = "READY" // SourceStatusUnavailable indicates the source is not available SourceStatusUnavailable SourceStatus = "UNAVAILABLE" // SourceStatusError indicates an error with the source SourceStatusError SourceStatus = "ERROR" ) // IsReady returns true if the source is ready for use func (ss SourceStatus) IsReady() bool { return ss == SourceStatusReady } // IsUnavailable returns true if the source is unavailable func (ss SourceStatus) IsUnavailable() bool { return ss == SourceStatusUnavailable } // String returns a human-readable string representation func (ss SourceStatus) String() string { switch ss { case SourceStatusReady: return "Ready" case SourceStatusUnavailable: return "Unavailable" case SourceStatusError: return "Error" default: return "Unknown" } } // UnmarshalXML implements custom XML unmarshaling with validation func (ss *SourceStatus) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var s string if err := d.DecodeElement(&s, &start); err != nil { return err } switch s { case string(SourceStatusReady), string(SourceStatusUnavailable), string(SourceStatusError): *ss = SourceStatus(s) default: *ss = SourceStatusUnavailable // Default fallback for unknown states } return nil } // GetDisplayName returns the best available display name for the source func (si *SourceItem) GetDisplayName() string { if si.DisplayName != "" { return si.DisplayName } if si.SourceAccount != "" && si.SourceAccount != si.Source { return si.SourceAccount } // Manually implement title case to replace deprecated strings.Title source := strings.ToLower(si.Source) if len(source) == 0 { return source } return strings.ToUpper(source[:1]) + source[1:] } // IsSpotify returns true if this is a Spotify source func (si *SourceItem) IsSpotify() bool { return si.Source == "SPOTIFY" } // IsBluetoothSource returns true if this is a Bluetooth source func (si *SourceItem) IsBluetoothSource() bool { return si.Source == "BLUETOOTH" } // IsAuxSource returns true if this is an AUX input source func (si *SourceItem) IsAuxSource() bool { return si.Source == "AUX" } // IsStreamingService returns true if this is an online streaming service func (si *SourceItem) IsStreamingService() bool { streamingSources := []string{"SPOTIFY", "PANDORA", "TUNEIN", "IHEARTRADIO", "AMAZON", "LOCAL_INTERNET_RADIO"} for _, source := range streamingSources { if si.Source == source { return true } } return false } // IsLocalSource returns true if this is a local input source func (si *SourceItem) IsLocalSource() bool { return si.IsLocal } // SupportsMultiroom returns true if this source supports multiroom playback func (si *SourceItem) SupportsMultiroom() bool { return si.MultiroomAllowed } // GetAvailableSources returns only sources that are ready for use func (s *Sources) GetAvailableSources() []SourceItem { var available []SourceItem for _, source := range s.SourceItem { if source.Status.IsReady() { available = append(available, source) } } return available } // GetSourcesByType returns sources filtered by source type func (s *Sources) GetSourcesByType(sourceType string) []SourceItem { var filtered []SourceItem for _, source := range s.SourceItem { if source.Source == sourceType { filtered = append(filtered, source) } } return filtered } // GetSpotifySources returns all Spotify sources (there can be multiple accounts) func (s *Sources) GetSpotifySources() []SourceItem { return s.GetSourcesByType("SPOTIFY") } // GetReadySpotifySources returns only ready Spotify sources func (s *Sources) GetReadySpotifySources() []SourceItem { var ready []SourceItem for _, source := range s.GetSpotifySources() { if source.Status.IsReady() { ready = append(ready, source) } } return ready } // GetStreamingSources returns all streaming service sources func (s *Sources) GetStreamingSources() []SourceItem { var streaming []SourceItem for _, source := range s.SourceItem { if source.IsStreamingService() { streaming = append(streaming, source) } } return streaming } // GetLocalSources returns all local input sources func (s *Sources) GetLocalSources() []SourceItem { var local []SourceItem for _, source := range s.SourceItem { if source.IsLocalSource() { local = append(local, source) } } return local } // GetMultiroomSources returns sources that support multiroom playback func (s *Sources) GetMultiroomSources() []SourceItem { var multiroom []SourceItem for _, source := range s.SourceItem { if source.SupportsMultiroom() { multiroom = append(multiroom, source) } } return multiroom } // HasSource returns true if the specified source type is available func (s *Sources) HasSource(sourceType string) bool { sources := s.GetSourcesByType(sourceType) for _, source := range sources { if source.Status.IsReady() { return true } } return false } // HasSpotify returns true if any Spotify source is ready func (s *Sources) HasSpotify() bool { return s.HasSource("SPOTIFY") } // HasBluetooth returns true if Bluetooth source is ready func (s *Sources) HasBluetooth() bool { return s.HasSource("BLUETOOTH") } // HasAux returns true if AUX input is ready func (s *Sources) HasAux() bool { return s.HasSource("AUX") } // GetSourceCount returns the total number of sources func (s *Sources) GetSourceCount() int { return len(s.SourceItem) } // GetReadySourceCount returns the number of ready sources func (s *Sources) GetReadySourceCount() int { return len(s.GetAvailableSources()) }