package models import ( "encoding/xml" "fmt" "time" ) // NowPlaying represents the current playback information from /now_playing endpoint type NowPlaying struct { XMLName xml.Name `xml:"nowPlaying"` DeviceID string `xml:"deviceID,attr"` Source string `xml:"source,attr"` SourceAccount string `xml:"sourceAccount,attr,omitempty"` ContentItem *ContentItem `xml:"ContentItem,omitempty"` Track string `xml:"track,omitempty"` Artist string `xml:"artist,omitempty"` Album string `xml:"album,omitempty"` StationName string `xml:"stationName,omitempty"` Art *Art `xml:"art,omitempty"` Time *Time `xml:"time,omitempty"` SkipEnabled *SkipEnabled `xml:"skipEnabled,omitempty"` FavoriteEnabled *FavoriteEnabled `xml:"favoriteEnabled,omitempty"` PlayStatus PlayStatus `xml:"playStatus,omitempty"` ShuffleSetting ShuffleSetting `xml:"shuffleSetting,omitempty"` RepeatSetting RepeatSetting `xml:"repeatSetting,omitempty"` SkipPreviousEnabled *SkipPreviousEnabled `xml:"skipPreviousEnabled,omitempty"` SeekSupported *SeekSupported `xml:"seekSupported,omitempty"` StreamType string `xml:"streamType,omitempty"` TrackID string `xml:"trackID,omitempty"` Position *Position `xml:"position,omitempty"` Description string `xml:"description,omitempty"` StationLocation string `xml:"stationLocation,omitempty"` } // ContentItem represents metadata about the currently playing content type ContentItem struct { Source string `xml:"source,attr"` Type string `xml:"type,attr"` Location string `xml:"location,attr"` SourceAccount string `xml:"sourceAccount,attr"` IsPresetable bool `xml:"isPresetable,attr"` ItemName string `xml:"itemName,omitempty"` ContainerArt string `xml:"containerArt,omitempty"` } // Art represents album artwork information type Art struct { ArtImageStatus string `xml:"artImageStatus,attr"` URL string `xml:",chardata"` } // Time represents playback time information with total duration and current position type Time struct { Total int `xml:"total,attr"` // Total duration in seconds Position int `xml:",chardata"` // Current position in seconds } // SkipEnabled indicates if skip functionality is enabled type SkipEnabled struct{} // FavoriteEnabled indicates if favorite functionality is enabled type FavoriteEnabled struct{} // SkipPreviousEnabled indicates if skip previous functionality is enabled type SkipPreviousEnabled struct{} // SeekSupported indicates if seek functionality is supported type SeekSupported struct { Value bool `xml:"value,attr"` } // Position represents playback position information (legacy field) type Position struct { Position int `xml:",chardata"` // Position in seconds } // PlayStatus represents the current playback state type PlayStatus string const ( // PlayStatusPlaying indicates the device is currently playing content PlayStatusPlaying PlayStatus = "PLAY_STATE" // PlayStatusPaused indicates the device is paused PlayStatusPaused PlayStatus = "PAUSE_STATE" // PlayStatusStopped indicates the device is stopped PlayStatusStopped PlayStatus = "STOP_STATE" // PlayStatusBuffering indicates the device is buffering content PlayStatusBuffering PlayStatus = "BUFFERING_STATE" // PlayStatusInvalidPlay indicates an invalid play state PlayStatusInvalidPlay PlayStatus = "INVALID_PLAY_STATE" // PlayStatusStandby indicates the device is in standby mode PlayStatusStandby PlayStatus = "STANDBY" ) // IsPlaying returns true if the device is currently playing func (ps PlayStatus) IsPlaying() bool { return ps == PlayStatusPlaying } // IsPaused returns true if the device is paused func (ps PlayStatus) IsPaused() bool { return ps == PlayStatusPaused } // IsStopped returns true if the device is stopped func (ps PlayStatus) IsStopped() bool { return ps == PlayStatusStopped } // String returns a human-readable string representation func (ps PlayStatus) String() string { switch ps { case PlayStatusPlaying: return "Playing" case PlayStatusPaused: return "Paused" case PlayStatusStopped: return "Stopped" case PlayStatusBuffering: return "Buffering" case PlayStatusInvalidPlay: return "Invalid" default: return "Unknown" } } // UnmarshalXML implements custom XML unmarshaling with validation func (ps *PlayStatus) 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(PlayStatusPlaying), string(PlayStatusPaused), string(PlayStatusStopped), string(PlayStatusBuffering), string(PlayStatusInvalidPlay): *ps = PlayStatus(s) default: *ps = PlayStatusStopped // Default fallback for unknown states } return nil } // ShuffleSetting represents shuffle mode state type ShuffleSetting string const ( // ShuffleOff indicates shuffle is disabled ShuffleOff ShuffleSetting = "SHUFFLE_OFF" // ShuffleOn indicates shuffle is enabled ShuffleOn ShuffleSetting = "SHUFFLE_ON" ) // IsEnabled returns true if shuffle is enabled func (ss ShuffleSetting) IsEnabled() bool { return ss == ShuffleOn } // String returns a human-readable string representation func (ss ShuffleSetting) String() string { switch ss { case ShuffleOn: return "On" case ShuffleOff: return "Off" default: return "Unknown" } } // UnmarshalXML implements custom XML unmarshaling func (ss *ShuffleSetting) 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(ShuffleOn), string(ShuffleOff): *ss = ShuffleSetting(s) default: *ss = ShuffleOff // Default fallback } return nil } // RepeatSetting represents repeat mode state type RepeatSetting string const ( // RepeatOff indicates no repeat mode RepeatOff RepeatSetting = "REPEAT_OFF" // RepeatOne indicates repeat current track RepeatOne RepeatSetting = "REPEAT_ONE" // RepeatAll indicates repeat all tracks RepeatAll RepeatSetting = "REPEAT_ALL" ) // IsEnabled returns true if any repeat mode is enabled func (rs RepeatSetting) IsEnabled() bool { return rs != RepeatOff } // String returns a human-readable string representation func (rs RepeatSetting) String() string { switch rs { case RepeatOff: return "Off" case RepeatOne: return "One" case RepeatAll: return "All" default: return "Unknown" } } // UnmarshalXML implements custom XML unmarshaling func (rs *RepeatSetting) 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(RepeatOff), string(RepeatOne), string(RepeatAll): *rs = RepeatSetting(s) default: *rs = RepeatOff // Default fallback } return nil } // IsEmpty returns true if no content is currently playing func (np *NowPlaying) IsEmpty() bool { return np.Track == "" && np.Artist == "" && np.Album == "" && np.StationName == "" } // HasTrackInfo returns true if the playing content has track metadata func (np *NowPlaying) HasTrackInfo() bool { return np.Track != "" || np.Artist != "" || np.Album != "" } // IsRadio returns true if the current source appears to be radio/streaming func (np *NowPlaying) IsRadio() bool { return np.StationName != "" || np.Source == "TUNEIN" || np.Source == "IHEARTRADIO" || np.Source == "PANDORA" } // GetDisplayTitle returns the best available title for display func (np *NowPlaying) GetDisplayTitle() string { if np.Track != "" { return np.Track } if np.StationName != "" { return np.StationName } if np.ContentItem != nil && np.ContentItem.ItemName != "" { return np.ContentItem.ItemName } return "Unknown" } // GetDisplayArtist returns the best available artist for display func (np *NowPlaying) GetDisplayArtist() string { if np.Artist != "" { return np.Artist } if np.Description != "" { return np.Description } return "" } // GetArtworkURL returns the artwork URL if available func (np *NowPlaying) GetArtworkURL() string { if np.Art != nil && np.Art.URL != "" { return np.Art.URL } if np.ContentItem != nil && np.ContentItem.ContainerArt != "" { return np.ContentItem.ContainerArt } return "" } // GetPositionDuration returns position as a time.Duration func (np *NowPlaying) GetPositionDuration() time.Duration { if np.Time != nil { return time.Duration(np.Time.Position) * time.Second } if np.Position != nil { return time.Duration(np.Position.Position) * time.Second } return 0 } // GetTotalDuration returns total duration as a time.Duration func (np *NowPlaying) GetTotalDuration() time.Duration { if np.Time != nil { return time.Duration(np.Time.Total) * time.Second } return 0 } // FormatPosition returns a formatted position string (MM:SS) func (np *NowPlaying) FormatPosition() string { if np.Time == nil && np.Position == nil { return "" } duration := np.GetPositionDuration() minutes := int(duration.Minutes()) seconds := int(duration.Seconds()) % 60 return fmt.Sprintf("%d:%02d", minutes, seconds) } // FormatDuration returns a formatted duration string (MM:SS) including total time func (np *NowPlaying) FormatDuration() string { position := np.FormatPosition() if position == "" { return "" } totalDuration := np.GetTotalDuration() if totalDuration == 0 { return position } totalMinutes := int(totalDuration.Minutes()) totalSeconds := int(totalDuration.Seconds()) % 60 return fmt.Sprintf("%s / %d:%02d", position, totalMinutes, totalSeconds) } // HasTimeInfo returns true if time/duration information is available func (np *NowPlaying) HasTimeInfo() bool { return np.Time != nil || np.Position != nil } // IsSeekSupported returns true if seeking is supported func (np *NowPlaying) IsSeekSupported() bool { return np.SeekSupported != nil && np.SeekSupported.Value } // CanSkip returns true if skip functionality is available func (np *NowPlaying) CanSkip() bool { return np.SkipEnabled != nil } // CanSkipPrevious returns true if skip previous functionality is available func (np *NowPlaying) CanSkipPrevious() bool { return np.SkipPreviousEnabled != nil } // CanFavorite returns true if favorite functionality is available func (np *NowPlaying) CanFavorite() bool { return np.FavoriteEnabled != nil }