package proxy import ( "encoding/base64" "encoding/json" "encoding/xml" "fmt" "strings" "github.com/gesellix/bose-soundtouch/pkg/service/constants" ) // SCMUDCRequest represents the structure of SCMUDC telemetry requests type SCMUDCRequest struct { Envelope struct { MonoTime int64 `json:"monoTime"` PayloadProtocolVersion string `json:"payloadProtocolVersion"` PayloadType string `json:"payloadType"` ProtocolVersion string `json:"protocolVersion"` Time string `json:"time"` UniqueID string `json:"uniqueId"` } `json:"envelope"` Payload struct { DeviceInfo struct { BoseID string `json:"boseID"` DeviceID string `json:"deviceID"` DeviceType string `json:"deviceType"` SerialNumber string `json:"serialNumber"` SoftwareVersion string `json:"softwareVersion"` SystemSerialNumber string `json:"systemSerialNumber"` } `json:"deviceInfo"` Events []SCMUDCEvent `json:"events"` } `json:"payload"` } // SCMUDCEvent represents individual events within SCMUDC requests type SCMUDCEvent struct { Type string `json:"type"` Data struct { ButtonID string `json:"buttonId,omitempty"` Origin string `json:"origin"` ContentItem string `json:"contentItem,omitempty"` Preset string `json:"preset,omitempty"` } `json:"data"` } // EnrichedSCMUDCEvent contains human-readable analysis of SCMUDC events type EnrichedSCMUDCEvent struct { Origin string `json:"origin"` Action string `json:"action"` Command string `json:"command"` Summary string `json:"summary"` DecodedData *DecodedContent `json:"decoded_data,omitempty"` } // DecodedContent represents decoded ContentItem XML data type DecodedContent struct { ContentType string `json:"content_type"` ItemName string `json:"item_name"` SourceAccount string `json:"source_account,omitempty"` Location string `json:"location,omitempty"` ArtworkURL string `json:"artwork_url,omitempty"` IsPresetable bool `json:"is_presetable,omitempty"` XMLContent string `json:"xml_content,omitempty"` } // ContentItem represents the XML structure within Base64-encoded content type ContentItem struct { XMLName xml.Name `xml:"ContentItem"` Source string `xml:"source,attr"` Type string `xml:"type,attr"` Location string `xml:"location,attr"` SourceAccount string `xml:"sourceAccount,attr"` IsPresetable string `xml:"isPresetable,attr"` ItemName string `xml:"itemName"` ContainerArt string `xml:"containerArt"` } // enrichSCMUDCRequest analyzes and enriches SCMUDC request data func enrichSCMUDCRequest(body []byte) *EnrichedSCMUDCEvent { var scmudcReq SCMUDCRequest if err := json.Unmarshal(body, &scmudcReq); err != nil { return nil } // Process the first event (most requests contain single events) if len(scmudcReq.Payload.Events) == 0 { return nil } event := scmudcReq.Payload.Events[0] enriched := &EnrichedSCMUDCEvent{ Origin: event.Data.Origin, Action: event.Type, } switch event.Data.Origin { case "gabbo": enriched.Command = event.Data.ButtonID enriched.Summary = fmt.Sprintf("App: %s", formatButton(event.Data.ButtonID)) case "console": enriched.Command = event.Data.ButtonID enriched.Summary = fmt.Sprintf("Device: %s", formatButton(event.Data.ButtonID)) case "device": if event.Data.ContentItem != "" { if decoded := decodeContentItem(event.Data.ContentItem); decoded != nil { enriched.Command = decoded.ItemName enriched.Summary = fmt.Sprintf("Device: %s", summarizeContent(decoded)) enriched.DecodedData = decoded } else { enriched.Command = "Content Item" enriched.Summary = "Device: Content Action" } } else { enriched.Command = "System Action" enriched.Summary = "Device: Internal Action" } } return enriched } // decodeContentItem decodes Base64-encoded XML content func decodeContentItem(base64Content string) *DecodedContent { data, err := base64.StdEncoding.DecodeString(base64Content) if err != nil { return nil } var contentItem ContentItem if err := xml.Unmarshal(data, &contentItem); err != nil { return nil } decoded := &DecodedContent{ ContentType: contentItem.Source, ItemName: contentItem.ItemName, SourceAccount: contentItem.SourceAccount, Location: contentItem.Location, ArtworkURL: contentItem.ContainerArt, XMLContent: string(data), } if contentItem.IsPresetable == "true" { decoded.IsPresetable = true } return decoded } // formatButton converts button IDs to human-readable names func formatButton(buttonID string) string { switch buttonID { case "POWER": return "Power Button" case "PLAY": return "Play Button" case "PAUSE": return "Pause Button" case "STOP": return "Stop Button" case "NEXT_TRACK": return "Next Track" case "PREV_TRACK": return "Previous Track" case "PRESET_1", "PRESET_2", "PRESET_3", "PRESET_4", "PRESET_5", "PRESET_6": return fmt.Sprintf("Preset %s", strings.TrimPrefix(buttonID, "PRESET_")) default: return buttonID } } // summarizeContent creates a brief summary of content items func summarizeContent(decoded *DecodedContent) string { switch decoded.ContentType { case constants.ProviderSpotify: return fmt.Sprintf("Spotify: %s", decoded.ItemName) case constants.ProviderPandora: return fmt.Sprintf("Pandora: %s", decoded.ItemName) case constants.ProviderInternetRadio: return fmt.Sprintf("Radio: %s", decoded.ItemName) case constants.ProviderStoredMusic: return fmt.Sprintf("Library: %s", decoded.ItemName) default: if decoded.ItemName != "" { return fmt.Sprintf("%s: %s", decoded.ContentType, decoded.ItemName) } return fmt.Sprintf("%s Content", decoded.ContentType) } } // getOriginDescription returns human-readable origin descriptions func getOriginDescription(origin string) string { switch origin { case "gabbo": return "SoundTouch App" case "console": return "Device Hardware" case "device": return "Internal System" default: return origin } } // generateSCMUDCComments creates enriched comments for .http files func generateSCMUDCComments(enriched *EnrichedSCMUDCEvent) []string { if enriched == nil { return nil } comments := []string{ fmt.Sprintf("// Origin: %s (%s)", getOriginDescription(enriched.Origin), enriched.Origin), fmt.Sprintf("// Action: %s", enriched.Action), fmt.Sprintf("// Command: %s", enriched.Command), fmt.Sprintf("// Summary: %s", enriched.Summary), } if enriched.DecodedData != nil { comments = append(comments, "//", "// Decoded Content:", fmt.Sprintf("// - Source: %s", enriched.DecodedData.ContentType), fmt.Sprintf("// - Item: %s", enriched.DecodedData.ItemName), ) if enriched.DecodedData.SourceAccount != "" { comments = append(comments, fmt.Sprintf("// - Account: %s", enriched.DecodedData.SourceAccount)) } if enriched.DecodedData.ArtworkURL != "" { comments = append(comments, fmt.Sprintf("// - Artwork: %s", enriched.DecodedData.ArtworkURL)) } if enriched.DecodedData.IsPresetable { comments = append(comments, "// - Presetable: Yes") } if enriched.DecodedData.XMLContent != "" { comments = append(comments, "//", "// Full XML Content:", ) // Add XML content as comments, line by line lines := strings.Split(enriched.DecodedData.XMLContent, "\n") for _, line := range lines { if strings.TrimSpace(line) != "" { comments = append(comments, fmt.Sprintf("// %s", strings.TrimSpace(line))) } } } } return comments }