mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 00:56:16 +00:00
## New Endpoints ### GET /now_playing ✅ - Rich XML models with PlayStatus, ShuffleSetting, RepeatSetting enums - Comprehensive playback information (track, artist, album, artwork, position) - Device capabilities (skip, seek, favorite functionality) - Smart display methods for different content types (music vs radio) - Duration formatting with position/total time display ### GET /sources ✅ - Complete audio source management with SourceStatus enum - Source categorization (Local/Remote, Streaming, Multiroom support) - Multiple account support (multiple Spotify accounts per device) - Availability filtering (Ready vs Unavailable sources) - Helper methods for quick capability checks ## Real Device Integration - Fetched actual XML responses from SoundTouch devices (192.168.178.28 & 192.168.178.35) - Updated all test fixtures with real device data (anonymized) - Enhanced XML models to handle all real-world fields and edge cases - Verified compatibility across different device types and configurations ## Enhanced CLI Tool - Added -nowplaying command with rich formatted output - Added -sources command with categorized source listing - Display enhancements: duration info, capabilities, source attributes - Improved build process to use ./build/ directory consistently ## Comprehensive Testing - 15+ unit tests for XML models with enum validation - Client integration tests with mock HTTP responses - Real device response validation - Edge case handling (empty states, network errors, invalid data) ## Documentation & Guidelines - Updated CLAUDE.md with build directory and real device testing guidelines - Enhanced README with comprehensive usage examples - Updated PLAN.md to reflect implementation progress - All examples use real device data patterns ## Quality Improvements - Type-safe XML unmarshaling with custom validation - Consistent error handling across all endpoints - Privacy protection (anonymized account information) - Production-ready code structure and patterns Features: ✅ GET /info - Device information ✅ GET /now_playing - Current playback status with full metadata ✅ GET /sources - Available audio sources with smart categorization ✅ UPnP device discovery ✅ Cross-platform CLI tool with rich output formatting ✅ Comprehensive test coverage with real device data ✅ Build automation with proper directory structure
227 lines
6.0 KiB
Go
227 lines
6.0 KiB
Go
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 SourceStatus = "READY"
|
|
SourceStatusUnavailable SourceStatus = "UNAVAILABLE"
|
|
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
|
|
}
|
|
return strings.Title(strings.ToLower(si.Source))
|
|
}
|
|
|
|
// 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())
|
|
}
|