Decode SCMUDC event details (#97)

This should help understanding events from the SoundTouch app to the
speakers and from speakers to the BMX service.
This commit is contained in:
Tobias Gesellchen
2026-03-05 23:19:39 +01:00
committed by GitHub
parent 1e24ca076a
commit eb50e9b6f6
6 changed files with 2896 additions and 1139 deletions
+17
View File
@@ -0,0 +1,17 @@
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.html]
# HTML-specific formatting
# Standardize on tag layout
ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot
ij_html_keep_blank_lines = 1
ij_html_attribute_wrap = normal
ij_html_space_inside_empty_tag = false
+323
View File
@@ -0,0 +1,323 @@
package handlers
import (
"encoding/base64"
"encoding/json"
"encoding/xml"
"fmt"
"strings"
"time"
)
// SCMUDCRequest represents the structure of incoming SCMUDC telemetry data
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 payload
type SCMUDCEvent struct {
Data SCMUDCEventData `json:"data"`
MonoTime int64 `json:"monoTime"`
Time string `json:"time"`
Type string `json:"type"`
}
// SCMUDCEventData contains the event-specific data
type SCMUDCEventData struct {
ButtonID string `json:"buttonId,omitempty"`
ContentItem string `json:"contentItem,omitempty"`
Origin string `json:"origin"`
Preset string `json:"preset,omitempty"`
}
// EnrichedSCMUDCEvent contains processed and human-readable event information
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"`
Timestamp time.Time `json:"timestamp"`
}
// DecodedContent represents decoded Base64 content from device events
type DecodedContent struct {
ContentType string `json:"content_type"`
ItemName string `json:"item_name"`
SourceAccount string `json:"source_account"`
Location string `json:"location"`
ArtworkURL string `json:"artwork_url,omitempty"`
IsPresetable bool `json:"is_presetable"`
XMLContent string `json:"xml_content"`
}
// ContentItemXML represents the XML structure found in Base64-encoded content
type ContentItemXML 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"`
}
// SCMUDCEnricher provides functionality to enrich SCMUDC event data
type SCMUDCEnricher struct{}
// NewSCMUDCEnricher creates a new SCMUDC enricher instance
func NewSCMUDCEnricher() *SCMUDCEnricher {
return &SCMUDCEnricher{}
}
// EnrichSCMUDCRequest processes a raw SCMUDC request and returns enriched event data
func (e *SCMUDCEnricher) EnrichSCMUDCRequest(body []byte) (*EnrichedSCMUDCEvent, error) {
var scmudcReq SCMUDCRequest
if err := json.Unmarshal(body, &scmudcReq); err != nil {
return nil, fmt.Errorf("failed to unmarshal SCMUDC request: %w", err)
}
// Process the first event (most requests contain single events)
if len(scmudcReq.Payload.Events) == 0 {
return nil, fmt.Errorf("no events found in SCMUDC request")
}
event := scmudcReq.Payload.Events[0]
// Parse timestamp
timestamp, _ := time.Parse(time.RFC3339, event.Time)
enriched := &EnrichedSCMUDCEvent{
Origin: event.Data.Origin,
Action: event.Type,
Timestamp: timestamp,
}
switch event.Data.Origin {
case "gabbo":
e.enrichAppEvent(enriched, &event)
case "console":
e.enrichConsoleEvent(enriched, &event)
case "device":
e.enrichDeviceEvent(enriched, &event)
default:
enriched.Command = "Unknown"
enriched.Summary = fmt.Sprintf("Unknown origin: %s", event.Data.Origin)
}
return enriched, nil
}
// enrichAppEvent processes events from the SoundTouch app
func (e *SCMUDCEnricher) enrichAppEvent(enriched *EnrichedSCMUDCEvent, event *SCMUDCEvent) {
enriched.Command = event.Data.ButtonID
enriched.Summary = fmt.Sprintf("App: %s", e.formatButton(event.Data.ButtonID))
}
// enrichConsoleEvent processes events from physical device controls
func (e *SCMUDCEnricher) enrichConsoleEvent(enriched *EnrichedSCMUDCEvent, event *SCMUDCEvent) {
enriched.Command = event.Data.ButtonID
enriched.Summary = fmt.Sprintf("Device: %s", e.formatButton(event.Data.ButtonID))
}
// enrichDeviceEvent processes internal device events with content data
func (e *SCMUDCEnricher) enrichDeviceEvent(enriched *EnrichedSCMUDCEvent, event *SCMUDCEvent) {
if event.Data.ContentItem != "" {
if decoded := e.decodeContentItem(event.Data.ContentItem); decoded != nil {
enriched.Command = decoded.ItemName
enriched.Summary = fmt.Sprintf("Device: %s", e.summarizeContent(decoded))
enriched.DecodedData = decoded
return
}
}
// Fallback for device events without content
enriched.Command = "System Action"
enriched.Summary = fmt.Sprintf("Device: %s", enriched.Action)
}
// decodeContentItem decodes Base64-encoded XML content from device events
func (e *SCMUDCEnricher) decodeContentItem(base64Content string) *DecodedContent {
data, err := base64.StdEncoding.DecodeString(base64Content)
if err != nil {
return nil
}
var contentItem ContentItemXML
if err := xml.Unmarshal(data, &contentItem); err != nil {
return nil
}
return &DecodedContent{
ContentType: contentItem.Source,
ItemName: contentItem.ItemName,
SourceAccount: contentItem.SourceAccount,
Location: contentItem.Location,
ArtworkURL: contentItem.ContainerArt,
IsPresetable: strings.EqualFold(contentItem.IsPresetable, "true"),
XMLContent: string(data),
}
}
// formatButton converts button IDs to human-readable names
func (e *SCMUDCEnricher) formatButton(buttonID string) string {
buttonNames := map[string]string{
"POWER": "Power",
"PLAY": "Play",
"PAUSE": "Pause",
"STOP": "Stop",
"NEXT_TRACK": "Skip Forward",
"PREV_TRACK": "Skip Backward",
"VOLUME_UP": "Volume Up",
"VOLUME_DOWN": "Volume Down",
"MUTE": "Mute",
"PRESET_1": "Preset 1",
"PRESET_2": "Preset 2",
"PRESET_3": "Preset 3",
"PRESET_4": "Preset 4",
"PRESET_5": "Preset 5",
"PRESET_6": "Preset 6",
}
if name, exists := buttonNames[buttonID]; exists {
return name
}
return buttonID
}
// summarizeContent creates a short summary of content for UI display
func (e *SCMUDCEnricher) summarizeContent(content *DecodedContent) string {
if content.ItemName != "" {
return fmt.Sprintf("Playing %s", e.truncateString(content.ItemName, 30))
}
if content.ContentType != "" {
return fmt.Sprintf("Playing %s content", content.ContentType)
}
return "Playing content"
}
// truncateString truncates a string to maxLength with ellipsis
func (e *SCMUDCEnricher) truncateString(s string, maxLength int) string {
if len(s) <= maxLength {
return s
}
return s[:maxLength-3] + "..."
}
// GetOriginDescription returns a human-readable description of the event origin
func (e *SCMUDCEnricher) GetOriginDescription(origin string) string {
descriptions := map[string]string{
"gabbo": "SoundTouch App",
"console": "Device Hardware",
"device": "Device Internal",
}
if desc, exists := descriptions[origin]; exists {
return desc
}
return "Unknown Origin"
}
// GenerateEnrichedComments creates comment lines for .http files
func (e *SCMUDCEnricher) GenerateEnrichedComments(enriched *EnrichedSCMUDCEvent) []string {
comments := []string{
fmt.Sprintf("// Origin: %s (%s)", e.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("// Track: %s", enriched.DecodedData.ItemName),
fmt.Sprintf("// Account: %s", enriched.DecodedData.SourceAccount),
)
if enriched.DecodedData.ArtworkURL != "" {
comments = append(comments, fmt.Sprintf("// Artwork: %s", enriched.DecodedData.ArtworkURL))
}
comments = append(comments,
"//",
"// Full XML:",
)
// Add XML content as comments, line by line
xmlLines := strings.Split(enriched.DecodedData.XMLContent, "\n")
for _, line := range xmlLines {
if strings.TrimSpace(line) != "" {
comments = append(comments, fmt.Sprintf("// %s", strings.TrimSpace(line)))
}
}
}
return comments
}
// IsSCMUDCRequest checks if a request path is a SCMUDC endpoint
func IsSCMUDCRequest(path string) bool {
return strings.Contains(path, "/v1/scmudc/")
}
// GetActionIcon returns an emoji icon for the given action type
func GetActionIcon(action string) string {
icons := map[string]string{
"play-pressed": "▶️",
"pause-pressed": "⏸️",
"power-pressed": "⚡",
"stop-pressed": "⏹️",
"skip-forward-pressed": "⏭️",
"skip-backward-pressed": "⏪",
"preset-pressed": "⭐",
"play-item": "🎵",
"preset-assigned": "🔖",
}
if icon, exists := icons[action]; exists {
return icon
}
return "🔘"
}
// GetOriginIcon returns an emoji icon for the given origin
func GetOriginIcon(origin string) string {
icons := map[string]string{
"gabbo": "📱", // SoundTouch App
"console": "🎛️", // Device Console
"device": "🔄", // Internal System
}
if icon, exists := icons[origin]; exists {
return icon
}
return "❓"
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+175 -18
View File
@@ -49,15 +49,16 @@ type InteractionStats struct {
// Interaction represents a single recorded HTTP interaction.
type Interaction struct {
ID string `json:"id"`
Session string `json:"session"`
Category string `json:"category"`
Method string `json:"method"`
Path string `json:"path"`
File string `json:"file"`
Counter int `json:"counter"`
Status int `json:"status"`
Timestamp string `json:"timestamp"`
ID string `json:"id"`
Session string `json:"session"`
Category string `json:"category"`
Method string `json:"method"`
Path string `json:"path"`
File string `json:"file"`
Counter int `json:"counter"`
Status int `json:"status"`
Timestamp string `json:"timestamp"`
SCMUDCData *EnrichedSCMUDCEvent `json:"scmudc_data,omitempty"`
}
// NewRecorder creates a new HTTP interaction recorder.
@@ -181,11 +182,25 @@ func (r *Recorder) Record(category string, req *http.Request, res *http.Response
}
func (r *Recorder) save(task recordingTask) {
var buf bytes.Buffer
r.writeRequest(&buf, task.req, task.replacements)
var (
buf bytes.Buffer
enriched *EnrichedSCMUDCEvent
)
// Check if this is a SCMUDC request and enrich it
if strings.Contains(task.req.URL.Path, "/v1/scmudc/") && task.req.Body != nil {
bodyBytes, err := io.ReadAll(task.req.Body)
if err == nil {
task.req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
enriched = enrichSCMUDCRequest(bodyBytes)
}
}
r.writeRequestWithEnrichment(&buf, task.req, task.replacements, enriched)
if task.res != nil {
r.writeResponse(&buf, task.res)
r.writeResponseWithEnrichment(&buf, task.res, enriched)
}
if err := os.WriteFile(task.path, buf.Bytes(), 0644); err != nil {
@@ -239,7 +254,7 @@ func (r *Recorder) getRecordingPath(dir, method string) string {
return filepath.Join(dir, filename)
}
func (r *Recorder) writeRequest(buf *bytes.Buffer, req *http.Request, replacements map[string]string) {
func (r *Recorder) writeRequestWithEnrichment(buf *bytes.Buffer, req *http.Request, replacements map[string]string, enriched *EnrichedSCMUDCEvent) {
displayURL := req.URL.String()
for orig, repl := range replacements {
displayURL = strings.ReplaceAll(displayURL, orig, "{{"+strings.Trim(repl, "{}")+"}}")
@@ -252,6 +267,14 @@ func (r *Recorder) writeRequest(buf *bytes.Buffer, req *http.Request, replacemen
fmt.Fprintf(buf, "// %s: %s\n", key, orig)
}
// Add SCMUDC enriched comments
if enriched != nil {
enrichedComments := generateSCMUDCComments(enriched)
for _, comment := range enrichedComments {
fmt.Fprintf(buf, "%s\n", comment)
}
}
fmt.Fprintf(buf, "%s %s\n", req.Method, displayURL)
fmt.Fprintf(buf, "Host: %s\n", req.Host)
@@ -283,10 +306,29 @@ func (r *Recorder) writeRequest(buf *bytes.Buffer, req *http.Request, replacemen
}
}
func (r *Recorder) writeResponse(buf *bytes.Buffer, res *http.Response) {
func (r *Recorder) writeResponseWithEnrichment(buf *bytes.Buffer, res *http.Response, enriched *EnrichedSCMUDCEvent) {
buf.WriteString("\n")
buf.WriteString("> {% \n")
fmt.Fprintf(buf, " // Response: %d %s\n", res.StatusCode, http.StatusText(res.StatusCode))
// Add SCMUDC enrichment summary in response
if enriched != nil {
buf.WriteString(" //\n")
buf.WriteString(" // SCMUDC Event Analysis:\n")
fmt.Fprintf(buf, " // - Origin: %s (%s)\n", getOriginDescription(enriched.Origin), enriched.Origin)
fmt.Fprintf(buf, " // - Action: %s\n", enriched.Action)
fmt.Fprintf(buf, " // - Summary: %s\n", enriched.Summary)
if enriched.DecodedData != nil {
fmt.Fprintf(buf, " // - Content: %s\n", enriched.DecodedData.ItemName)
if enriched.DecodedData.SourceAccount != "" {
fmt.Fprintf(buf, " // - Account: %s\n", enriched.DecodedData.SourceAccount)
}
}
}
buf.WriteString(" //\n")
buf.WriteString(" // Headers:\n")
for k, vv := range res.Header {
@@ -312,8 +354,8 @@ func (r *Recorder) writeResponse(buf *bytes.Buffer, res *http.Response) {
buf.WriteString("\n/*\n")
buf.Write(bodyBytes)
buf.WriteString("\n*/\n")
} else {
fmt.Fprintf(buf, "\n// [Binary response body: %d bytes]\n", len(bodyBytes))
} else if len(bodyBytes) > 0 {
fmt.Fprintf(buf, "\n[Binary response body: %d bytes]\n", len(bodyBytes))
}
}
}
@@ -503,7 +545,7 @@ func (r *Recorder) parseInteractionFile(rel, path string, parts []string) (Inter
requestPath = "/"
}
return Interaction{
interaction := Interaction{
ID: filename,
Session: sessionID,
Category: category,
@@ -513,7 +555,122 @@ func (r *Recorder) parseInteractionFile(rel, path string, parts []string) (Inter
Counter: counter,
Status: r.peekStatus(path),
Timestamp: timestamp,
}, true
}
// Extract SCMUDC enrichment data if this is a SCMUDC request
if strings.Contains(requestPath, "/v1/scmudc/") {
interaction.SCMUDCData = r.extractSCMUDCFromFile(path)
}
return interaction, true
}
// extractSCMUDCFromFile parses SCMUDC enrichment data from a .http file
func (r *Recorder) extractSCMUDCFromFile(path string) *EnrichedSCMUDCEvent {
content, err := os.ReadFile(path)
if err != nil {
return nil
}
lines := strings.Split(string(content), "\n")
var (
enriched EnrichedSCMUDCEvent
foundSCMUDC bool
bodyStart int
)
// Look for SCMUDC enrichment comments
for i, line := range lines {
line = strings.TrimSpace(line)
switch {
case strings.HasPrefix(line, "// Origin: "):
parts := strings.Split(line, " (")
if len(parts) >= 2 {
enriched.Origin = strings.TrimSuffix(parts[1], ")")
foundSCMUDC = true
}
case strings.HasPrefix(line, "// Action: "):
enriched.Action = strings.TrimPrefix(line, "// Action: ")
case strings.HasPrefix(line, "// Command: "):
enriched.Command = strings.TrimPrefix(line, "// Command: ")
case strings.HasPrefix(line, "// Summary: "):
enriched.Summary = strings.TrimPrefix(line, "// Summary: ")
case strings.HasPrefix(line, "// - Source: "):
r.ensureDecodedData(&enriched)
enriched.DecodedData.ContentType = strings.TrimPrefix(line, "// - Source: ")
case strings.HasPrefix(line, "// - Item: "):
r.ensureDecodedData(&enriched)
enriched.DecodedData.ItemName = strings.TrimPrefix(line, "// - Item: ")
case strings.HasPrefix(line, "// - Account: "):
r.ensureDecodedData(&enriched)
enriched.DecodedData.SourceAccount = strings.TrimPrefix(line, "// - Account: ")
case strings.HasPrefix(line, "// - Artwork: "):
r.ensureDecodedData(&enriched)
enriched.DecodedData.ArtworkURL = strings.TrimPrefix(line, "// - Artwork: ")
case line == "// - Presetable: Yes":
r.ensureDecodedData(&enriched)
enriched.DecodedData.IsPresetable = true
case line == "{" && i > 0:
// Found start of JSON body
bodyStart = i
goto endLoop
}
}
endLoop:
// If we didn't find enrichment comments but this is a SCMUDC request,
// try to parse the JSON body directly
if !foundSCMUDC && bodyStart > 0 {
if parsed := r.parseSCMUDBody(lines, bodyStart); parsed != nil {
return parsed
}
}
if !foundSCMUDC {
return nil
}
return &enriched
}
func (r *Recorder) ensureDecodedData(enriched *EnrichedSCMUDCEvent) {
if enriched.DecodedData == nil {
enriched.DecodedData = &DecodedContent{}
}
}
func (r *Recorder) parseSCMUDBody(lines []string, bodyStart int) *EnrichedSCMUDCEvent {
var bodyLines []string
inBody := false
braceCount := 0
for i := bodyStart; i < len(lines); i++ {
line := lines[i]
if strings.TrimSpace(line) == "{" && !inBody {
inBody = true
bodyLines = append(bodyLines, line)
braceCount = 1
} else if inBody {
bodyLines = append(bodyLines, line)
braceCount += strings.Count(line, "{") - strings.Count(line, "}")
if braceCount == 0 {
break
}
}
}
if len(bodyLines) > 0 {
bodyJSON := strings.Join(bodyLines, "\n")
return enrichSCMUDCRequest([]byte(bodyJSON))
}
return nil
}
func (r *Recorder) getFullTimestamp(sessionID, filename string) string {
+257
View File
@@ -0,0 +1,257 @@
package proxy
import (
"encoding/base64"
"encoding/json"
"encoding/xml"
"fmt"
"strings"
)
// 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 "SPOTIFY":
return fmt.Sprintf("Spotify: %s", decoded.ItemName)
case "PANDORA":
return fmt.Sprintf("Pandora: %s", decoded.ItemName)
case "INTERNET_RADIO":
return fmt.Sprintf("Radio: %s", decoded.ItemName)
case "STORED_MUSIC":
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
}