mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 00:56:16 +00:00
Add/improve e2e test cases (#144)
https://github.com/gesellix/Bose-SoundTouch/issues/135
This commit is contained in:
@@ -135,8 +135,11 @@ test-http-client:
|
||||
/workdir/get_provider_settings.http \
|
||||
/workdir/tunein_playback_station.http \
|
||||
/workdir/set_preset_6.http \
|
||||
/workdir/get_presets.http \
|
||||
/workdir/delete_preset_6.http \
|
||||
/workdir/set_preset_5.http \
|
||||
/workdir/post_recent.http \
|
||||
/workdir/get_recents.http \
|
||||
/workdir/get_full_account.http \
|
||||
/workdir/get_group.http \
|
||||
/workdir/unregister_device.http \
|
||||
|
||||
@@ -747,7 +747,9 @@ func setupRouter(server *handlers.Server) *chi.Mux {
|
||||
r.Get("/presets", server.HandleMargePresets)
|
||||
r.Post("/presets/{presetNumber}", server.HandleMargeUpdatePreset)
|
||||
r.Put("/preset/{presetNumber}", server.HandleMargeUpdatePreset)
|
||||
r.Delete("/preset/{presetNumber}", server.HandleMargeRemovePreset)
|
||||
r.Get("/recent", server.HandleMargeRecents)
|
||||
r.Get("/recents", server.HandleMargeRecents)
|
||||
r.Post("/recent", server.HandleMargeAddRecent)
|
||||
|
||||
r.Get("/group", server.HandleMargeDeviceGroup)
|
||||
@@ -755,6 +757,8 @@ func setupRouter(server *handlers.Server) *chi.Mux {
|
||||
r.Get("/group/server", server.HandleMargeDeviceGroupServer)
|
||||
r.Get("/group/member", server.HandleMargeDeviceGroupMember)
|
||||
})
|
||||
|
||||
r.Delete("/device/{device}", server.HandleMargeRemoveDevice)
|
||||
})
|
||||
|
||||
r.Get("/device/{device}/streaming_token", server.HandleMargeStreamingToken)
|
||||
|
||||
@@ -6,6 +6,7 @@ DELETE /setup/dns-discoveries handlers.(
|
||||
DELETE /setup/interactions/sessions handlers.(*Server).HandleCleanupSessions-fm
|
||||
DELETE /setup/interactions/sessions/{session} handlers.(*Server).HandleDeleteSession-fm
|
||||
DELETE /setup/parity-mismatches handlers.(*Server).HandleClearParityMismatches-fm
|
||||
DELETE /streaming/account/{account}/device/{device}/preset/{presetNumber} handlers.(*Server).HandleMargeRemovePreset-fm
|
||||
GET / handlers.(*Server).HandleRoot-fm
|
||||
GET /accounts/{account}/devices/{device}/group handlers.(*Server).HandleMargeDeviceGroup-fm
|
||||
GET /accounts/{account}/devices/{device}/group/ handlers.(*Server).HandleMargeDeviceGroup-fm
|
||||
@@ -55,6 +56,7 @@ GET /streaming/account/{account}/device/{device}/group/member handlers.(
|
||||
GET /streaming/account/{account}/device/{device}/group/server handlers.(*Server).HandleMargeDeviceGroupServer-fm
|
||||
GET /streaming/account/{account}/device/{device}/presets handlers.(*Server).HandleMargePresets-fm
|
||||
GET /streaming/account/{account}/device/{device}/recent handlers.(*Server).HandleMargeRecents-fm
|
||||
GET /streaming/account/{account}/device/{device}/recents handlers.(*Server).HandleMargeRecents-fm
|
||||
GET /streaming/account/{account}/emailaddress handlers.(*Server).HandleMargeGetEmailAddress-fm
|
||||
GET /streaming/account/{account}/full handlers.(*Server).HandleMargeAccountFull-fm
|
||||
GET /streaming/account/{account}/provider_settings handlers.(*Server).HandleMargeProviderSettings-fm
|
||||
|
||||
@@ -197,6 +197,10 @@ func (p ServicePreset) MarshalXML(e *xml.Encoder, start xml.StartElement) error
|
||||
Username: p.Username,
|
||||
}
|
||||
|
||||
if a.Username == "" && a.Name != "" {
|
||||
a.Username = a.Name
|
||||
}
|
||||
|
||||
start.Name.Local = "preset"
|
||||
// Remove all attributes because they are handled in Alias
|
||||
start.Attr = nil
|
||||
|
||||
@@ -1000,16 +1000,44 @@ func (ds *DataStore) GetConfiguredSources(account, device string) ([]models.Conf
|
||||
return nil, err
|
||||
}
|
||||
|
||||
type persistentSource struct {
|
||||
DisplayName string `xml:"displayName,attr,omitempty"`
|
||||
ID string `xml:"id,attr,omitempty"`
|
||||
Secret string `xml:"secret,attr"`
|
||||
SecretType string `xml:"secretType,attr"`
|
||||
Type string `xml:"type,attr,omitempty"`
|
||||
CreatedOn string `xml:"createdOn,attr,omitempty"`
|
||||
UpdatedOn string `xml:"updatedOn,attr,omitempty"`
|
||||
SourceProviderID string `xml:"sourceproviderid,attr,omitempty"`
|
||||
SourceKey struct {
|
||||
Type string `xml:"type,attr"`
|
||||
Account string `xml:"account,attr"`
|
||||
} `xml:"sourceKey"`
|
||||
}
|
||||
|
||||
var sourcesWrap struct {
|
||||
Sources []models.ConfiguredSource `xml:"source"`
|
||||
Sources []persistentSource `xml:"source"`
|
||||
}
|
||||
|
||||
if err := xml.Unmarshal(data, &sourcesWrap); err != nil {
|
||||
return nil, fmt.Errorf("malformed sources XML at %s: %w", path, err)
|
||||
}
|
||||
|
||||
sources := make([]models.ConfiguredSource, len(sourcesWrap.Sources))
|
||||
for i := range sourcesWrap.Sources {
|
||||
s := &sourcesWrap.Sources[i]
|
||||
ps := &sourcesWrap.Sources[i]
|
||||
s := &sources[i]
|
||||
|
||||
s.DisplayName = ps.DisplayName
|
||||
s.ID = ps.ID
|
||||
s.Secret = ps.Secret
|
||||
s.SecretType = ps.SecretType
|
||||
s.Type = ps.Type
|
||||
s.CreatedOn = ps.CreatedOn
|
||||
s.UpdatedOn = ps.UpdatedOn
|
||||
s.SourceProviderID = ps.SourceProviderID
|
||||
s.SourceKey.Type = ps.SourceKey.Type
|
||||
s.SourceKey.Account = ps.SourceKey.Account
|
||||
|
||||
// Ensure Secret/SecretType values are prioritized from legacy fields
|
||||
if s.Secret == "" && s.Credential.Value != "" {
|
||||
@@ -1039,7 +1067,7 @@ func (ds *DataStore) GetConfiguredSources(account, device string) ([]models.Conf
|
||||
}
|
||||
}
|
||||
|
||||
return sourcesWrap.Sources, nil
|
||||
return sources, nil
|
||||
}
|
||||
|
||||
// SaveConfiguredSources saves the configured sources list for the specified account and device.
|
||||
@@ -1067,16 +1095,11 @@ func (ds *DataStore) SaveConfiguredSources(account, device string, sources []mod
|
||||
} `xml:"sourceKey"`
|
||||
}
|
||||
|
||||
type sourcesWrap struct {
|
||||
XMLName xml.Name `xml:"sources"`
|
||||
Sources []persistentSource `xml:"source"`
|
||||
}
|
||||
|
||||
// Ensure SourceKey is populated from legacy fields if necessary before saving
|
||||
// and map to persistentSource to avoid custom MarshalXML for disk storage
|
||||
persistSources := make([]persistentSource, len(sources))
|
||||
for i := range sources {
|
||||
s := &sources[i]
|
||||
s := sources[i]
|
||||
if s.SourceKey.Type == "" && s.SourceKeyType != "" {
|
||||
s.SourceKey.Type = s.SourceKeyType
|
||||
}
|
||||
@@ -1095,6 +1118,7 @@ func (ds *DataStore) SaveConfiguredSources(account, device string, sources []mod
|
||||
UpdatedOn: s.UpdatedOn,
|
||||
SourceProviderID: s.SourceProviderID,
|
||||
}
|
||||
|
||||
if persistSources[i].Secret == "" && s.Credential.Value != "" {
|
||||
persistSources[i].Secret = s.Credential.Value
|
||||
}
|
||||
@@ -1107,7 +1131,10 @@ func (ds *DataStore) SaveConfiguredSources(account, device string, sources []mod
|
||||
persistSources[i].SourceKey.Account = s.SourceKey.Account
|
||||
}
|
||||
|
||||
wrap := sourcesWrap{
|
||||
wrap := struct {
|
||||
XMLName xml.Name `xml:"sources"`
|
||||
Sources []persistentSource `xml:"source"`
|
||||
}{
|
||||
Sources: persistSources,
|
||||
}
|
||||
|
||||
|
||||
@@ -370,20 +370,26 @@ func TestConfiguredSources(t *testing.T) {
|
||||
t.Fatalf("Expected %d sources, got %d", len(sources), len(loadedSources))
|
||||
}
|
||||
|
||||
for i, s := range sources {
|
||||
for i := range sources {
|
||||
ls := loadedSources[i]
|
||||
s.Secret = ""
|
||||
s.SecretType = ""
|
||||
s.Type = ls.Type // Ignore Type mismatch in this test if it's auto-populated
|
||||
if ls.DisplayName != s.DisplayName || ls.ID != s.ID || ls.Secret != s.Secret ||
|
||||
ls.SecretType != s.SecretType || ls.SourceKeyType != s.SourceKeyType ||
|
||||
ls.SourceKeyAccount != s.SourceKeyAccount || ls.Type != s.Type {
|
||||
expected := sources[i]
|
||||
expected.Secret = ""
|
||||
expected.SecretType = ""
|
||||
expected.Type = ls.Type // Ignore Type mismatch in this test if it's auto-populated
|
||||
|
||||
// Clear secrets for comparison since they are not loaded by GetConfiguredSources
|
||||
ls.Secret = ""
|
||||
ls.SecretType = ""
|
||||
|
||||
if ls.DisplayName != expected.DisplayName || ls.ID != expected.ID || ls.Secret != expected.Secret ||
|
||||
ls.SecretType != expected.SecretType || ls.SourceKeyType != expected.SourceKeyType ||
|
||||
ls.SourceKeyAccount != expected.SourceKeyAccount || ls.Type != expected.Type {
|
||||
// Clean XMLName for comparison
|
||||
ls.XMLName = xml.Name{}
|
||||
if ls.DisplayName != s.DisplayName || ls.ID != s.ID || ls.Secret != s.Secret ||
|
||||
ls.SecretType != s.SecretType || ls.SourceKeyType != s.SourceKeyType ||
|
||||
ls.SourceKeyAccount != s.SourceKeyAccount || ls.Type != s.Type {
|
||||
t.Errorf("Source %d mismatch. Expected %+v, got %+v", i, s, ls)
|
||||
if ls.DisplayName != expected.DisplayName || ls.ID != expected.ID || ls.Secret != expected.Secret ||
|
||||
ls.SecretType != expected.SecretType || ls.SourceKeyType != expected.SourceKeyType ||
|
||||
ls.SourceKeyAccount != expected.SourceKeyAccount || ls.Type != expected.Type {
|
||||
t.Errorf("Source %d mismatch. Expected %+v, got %+v", i, expected, ls)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -496,6 +496,36 @@ func (s *Server) HandleMargeAddDevice(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
|
||||
// HandleMargeRemovePreset removes a preset for the specified account and device.
|
||||
func (s *Server) HandleMargeRemovePreset(w http.ResponseWriter, r *http.Request) {
|
||||
account := chi.URLParam(r, "account")
|
||||
if !validatePathID(account) {
|
||||
http.Error(w, "Invalid account ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
device := chi.URLParam(r, "device")
|
||||
if !validatePathID(device) {
|
||||
http.Error(w, "Invalid device ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
presetStr := chi.URLParam(r, "presetNumber")
|
||||
|
||||
presetNumber, err := strconv.Atoi(presetStr)
|
||||
if err != nil || presetNumber < 1 || presetNumber > 6 {
|
||||
http.Error(w, "Invalid preset number", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := marge.RemovePreset(s.ds, account, device, presetNumber); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// HandleMargeRemoveDevice removes a device from a Marge account.
|
||||
func (s *Server) HandleMargeRemoveDevice(w http.ResponseWriter, r *http.Request) {
|
||||
account := chi.URLParam(r, "account")
|
||||
|
||||
+281
-31
@@ -86,7 +86,14 @@ func GetConfiguredSourceXML(cs models.ConfiguredSource) string {
|
||||
|
||||
// PrepareConfiguredSource sets up the source for XML marshaling.
|
||||
func PrepareConfiguredSource(s *models.ConfiguredSource) {
|
||||
// Ensure dates are populated
|
||||
ensureTimestamps(s)
|
||||
ensureSourceType(s)
|
||||
ensureSourceProviderID(s)
|
||||
syncCredentials(s)
|
||||
syncLegacySourceKey(s)
|
||||
}
|
||||
|
||||
func ensureTimestamps(s *models.ConfiguredSource) {
|
||||
if s.CreatedOn == "" {
|
||||
s.CreatedOn = constants.DateStr
|
||||
}
|
||||
@@ -94,13 +101,15 @@ func PrepareConfiguredSource(s *models.ConfiguredSource) {
|
||||
if s.UpdatedOn == "" {
|
||||
s.UpdatedOn = constants.DateStr
|
||||
}
|
||||
}
|
||||
|
||||
// Default type for media sources
|
||||
func ensureSourceType(s *models.ConfiguredSource) {
|
||||
if s.Type == "" || (s.SourceKey.Type != "" && s.SourceKey.Type != "AUX" && s.SourceKey.Type != "BLUETOOTH") {
|
||||
s.Type = "Audio"
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure SourceProviderID is populated if possible
|
||||
func ensureSourceProviderID(s *models.ConfiguredSource) {
|
||||
if s.SourceProviderID == "" && s.SourceKey.Type != "" {
|
||||
for _, p := range constants.StaticProviders {
|
||||
if p.Name == s.SourceKey.Type {
|
||||
@@ -109,8 +118,9 @@ func PrepareConfiguredSource(s *models.ConfiguredSource) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Map secret types
|
||||
func syncCredentials(s *models.ConfiguredSource) {
|
||||
if s.SecretType == "" {
|
||||
if s.SourceKey.Type == "SPOTIFY" {
|
||||
s.SecretType = "token_version_3"
|
||||
@@ -127,7 +137,16 @@ func PrepareConfiguredSource(s *models.ConfiguredSource) {
|
||||
s.Credential.Value = s.Secret
|
||||
}
|
||||
|
||||
// Ensure SourceKey fields are synced with legacy fields if they were used
|
||||
if s.Secret == "" && s.Credential.Value != "" {
|
||||
s.Secret = s.Credential.Value
|
||||
}
|
||||
|
||||
if s.SecretType == "" && s.Credential.Type != "" {
|
||||
s.SecretType = s.Credential.Type
|
||||
}
|
||||
}
|
||||
|
||||
func syncLegacySourceKey(s *models.ConfiguredSource) {
|
||||
if s.SourceKey.Type == "" && s.SourceKeyType != "" {
|
||||
s.SourceKey.Type = s.SourceKeyType
|
||||
}
|
||||
@@ -137,6 +156,94 @@ func PrepareConfiguredSource(s *models.ConfiguredSource) {
|
||||
}
|
||||
}
|
||||
|
||||
// PresetsXML is the XML wrapper for a list of presets.
|
||||
type PresetsXML struct {
|
||||
XMLName xml.Name `xml:"presets"`
|
||||
Presets []models.ServicePreset `xml:"preset"`
|
||||
}
|
||||
|
||||
type presetParityXML struct {
|
||||
ButtonNumber string `xml:"buttonNumber,attr,omitempty"`
|
||||
ContainerArt string `xml:"containerArt"`
|
||||
ContentItemType string `xml:"contentItemType"`
|
||||
CreatedOn string `xml:"createdOn"`
|
||||
Location string `xml:"location"`
|
||||
Name string `xml:"name"`
|
||||
Source *models.ConfiguredSource `xml:"source,omitempty"`
|
||||
SourceID string `xml:"sourceid,omitempty"`
|
||||
UpdatedOn string `xml:"updatedOn"`
|
||||
Username string `xml:"username"`
|
||||
}
|
||||
|
||||
func (p presetParityXML) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
type Alias presetParityXML
|
||||
|
||||
start.Name.Local = "preset"
|
||||
|
||||
return e.EncodeElement(Alias(p), start)
|
||||
}
|
||||
|
||||
func prepareRecentItemParitySource(src *models.ConfiguredSource) *models.RecentItemParitySource {
|
||||
sxml := &models.RecentItemParitySource{
|
||||
ID: src.ID,
|
||||
Type: src.Type,
|
||||
CreatedOn: src.CreatedOn,
|
||||
UpdatedOn: src.UpdatedOn,
|
||||
Name: src.DisplayName,
|
||||
SourceProviderID: src.SourceProviderID,
|
||||
SourceName: src.SourceName,
|
||||
Username: src.Username,
|
||||
Credential: &models.RecentItemParityCredential{
|
||||
Type: src.Credential.Type,
|
||||
Value: src.Credential.Value,
|
||||
},
|
||||
}
|
||||
|
||||
if sxml.Name == "TuneIn" || sxml.Name == "LOCAL_INTERNET_RADIO" {
|
||||
sxml.Name = ""
|
||||
}
|
||||
|
||||
secret := src.Secret
|
||||
if secret == "" {
|
||||
secret = src.Credential.Value
|
||||
}
|
||||
|
||||
secretType := src.SecretType
|
||||
if secretType == "" {
|
||||
secretType = src.Credential.Type
|
||||
}
|
||||
|
||||
if secretType == "" {
|
||||
secretType = "token"
|
||||
}
|
||||
|
||||
if sxml.Credential.Value == "" {
|
||||
sxml.Credential.Value = secret
|
||||
}
|
||||
|
||||
if sxml.Credential.Type == "" {
|
||||
sxml.Credential.Type = secretType
|
||||
}
|
||||
|
||||
if sxml.SourceName == "" {
|
||||
sxml.SourceName = src.SourceKeyType
|
||||
}
|
||||
|
||||
if sxml.SourceName == "" {
|
||||
sxml.SourceName = sxml.Username
|
||||
}
|
||||
|
||||
if sxml.Username == "" {
|
||||
sxml.Username = sxml.SourceName
|
||||
}
|
||||
|
||||
if sxml.Name == "" {
|
||||
sxml.Name = sxml.SourceName
|
||||
}
|
||||
|
||||
return sxml
|
||||
}
|
||||
|
||||
// PresetsToXML converts account presets to XML format for Marge responses.
|
||||
func PresetsToXML(ds *datastore.DataStore, account, deviceID string) ([]byte, error) {
|
||||
presets, err := ds.GetPresets(account, deviceID)
|
||||
@@ -149,34 +256,63 @@ func PresetsToXML(ds *datastore.DataStore, account, deviceID string) ([]byte, er
|
||||
return nil, err
|
||||
}
|
||||
|
||||
type PresetsXML struct {
|
||||
XMLName xml.Name `xml:"presets"`
|
||||
Presets []models.ServicePreset `xml:"preset"`
|
||||
type presetsParityWrapper struct {
|
||||
XMLName xml.Name `xml:"presets"`
|
||||
Presets []presetParityXML `xml:"preset"`
|
||||
}
|
||||
|
||||
pxml := PresetsXML{
|
||||
Presets: make([]models.ServicePreset, 0, len(presets)),
|
||||
pxml := presetsParityWrapper{
|
||||
Presets: make([]presetParityXML, 0, len(presets)),
|
||||
}
|
||||
|
||||
for i := range presets {
|
||||
p := presets[i]
|
||||
|
||||
// Find and prepare source
|
||||
matchedSource := findMatchingSourceForPreset(sources, p)
|
||||
if matchedSource != nil {
|
||||
PrepareConfiguredSource(matchedSource)
|
||||
}
|
||||
|
||||
if p.ContentItemType == "" && p.Name == "" && p.Location == "" && (matchedSource == nil || matchedSource.ID == "") {
|
||||
continue
|
||||
}
|
||||
|
||||
p.ButtonNumber = p.ID
|
||||
if p.CreatedOn == "" {
|
||||
p.CreatedOn = constants.DateStr
|
||||
} else if t, e := strconv.ParseInt(p.CreatedOn, 10, 64); e == nil {
|
||||
p.CreatedOn = time.Unix(t, 0).UTC().Format("2006-01-02T15:04:05.000+00:00")
|
||||
}
|
||||
|
||||
if p.UpdatedOn == "" {
|
||||
p.UpdatedOn = constants.DateStr
|
||||
} else if t, e := strconv.ParseInt(p.UpdatedOn, 10, 64); e == nil {
|
||||
p.UpdatedOn = time.Unix(t, 0).UTC().Format("2006-01-02T15:04:05.000+00:00")
|
||||
}
|
||||
|
||||
// Find and prepare source
|
||||
if matchedSource := findMatchingSourceForPreset(sources, p); matchedSource != nil {
|
||||
PrepareConfiguredSource(matchedSource)
|
||||
p.SourceConfig = matchedSource
|
||||
username := p.Username
|
||||
if username == "" {
|
||||
username = p.Name
|
||||
}
|
||||
|
||||
pxml.Presets = append(pxml.Presets, p)
|
||||
sourceID := p.SourceID
|
||||
if sourceID == "" && matchedSource != nil {
|
||||
sourceID = matchedSource.ID
|
||||
}
|
||||
|
||||
pxml.Presets = append(pxml.Presets, presetParityXML{
|
||||
ButtonNumber: p.ButtonNumber,
|
||||
ContainerArt: p.ContainerArt,
|
||||
ContentItemType: p.ContentItemType,
|
||||
CreatedOn: p.CreatedOn,
|
||||
Location: p.Location,
|
||||
Name: p.Name,
|
||||
Source: matchedSource,
|
||||
SourceID: sourceID,
|
||||
UpdatedOn: p.UpdatedOn,
|
||||
Username: username,
|
||||
})
|
||||
}
|
||||
|
||||
data, err := xml.MarshalIndent(pxml, "", " ")
|
||||
@@ -211,33 +347,38 @@ func RecentsToXML(ds *datastore.DataStore, account, deviceID string) ([]byte, er
|
||||
return nil, err
|
||||
}
|
||||
|
||||
type RecentsXML struct {
|
||||
XMLName xml.Name `xml:"recents"`
|
||||
Recents []models.ServiceRecent `xml:"recent"`
|
||||
type recentsParityXML struct {
|
||||
XMLName xml.Name `xml:"recents"`
|
||||
Recents []recent `xml:"recent"`
|
||||
}
|
||||
|
||||
rxml := RecentsXML{
|
||||
Recents: recents,
|
||||
rxml := recentsParityXML{
|
||||
Recents: make([]recent, len(recents)),
|
||||
}
|
||||
|
||||
for i := range rxml.Recents {
|
||||
r := &rxml.Recents[i]
|
||||
sources, _ := ds.GetConfiguredSources(account, deviceID)
|
||||
|
||||
for i := range recents {
|
||||
r := &recents[i]
|
||||
if r.SourceConfig == nil && r.SourceID != "" {
|
||||
sources, err2 := ds.GetConfiguredSources(account, deviceID)
|
||||
if err2 == nil {
|
||||
r.SourceConfig = findMatchingSource(sources, r.SourceID)
|
||||
}
|
||||
r.SourceConfig = findMatchingSource(sources, r.SourceID)
|
||||
}
|
||||
|
||||
if r.SourceConfig != nil {
|
||||
PrepareConfiguredSource(r.SourceConfig)
|
||||
}
|
||||
} else if r.Source != "" {
|
||||
// Try to find by Source and SourceAccount if SourceID didn't match
|
||||
for j := range sources {
|
||||
if sources[j].SourceKeyType == r.Source && sources[j].SourceKeyAccount == r.SourceAccount {
|
||||
r.SourceConfig = &sources[j]
|
||||
PrepareConfiguredSource(r.SourceConfig)
|
||||
|
||||
if r.UtcTime != "" {
|
||||
if t, parseErr := strconv.ParseInt(r.UtcTime, 10, 64); parseErr == nil {
|
||||
r.LastPlayedAt = time.Unix(t, 0).UTC().Format("2006-01-02T15:04:05.000+00:00")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rxml.Recents[i] = recentToXML(r)
|
||||
}
|
||||
|
||||
data, err := xml.MarshalIndent(rxml, "", " ")
|
||||
@@ -253,6 +394,80 @@ func RecentsToXML(ds *datastore.DataStore, account, deviceID string) ([]byte, er
|
||||
return append([]byte(header+"\n"), data...), nil
|
||||
}
|
||||
|
||||
type recent struct {
|
||||
ID string `xml:"id,attr"`
|
||||
ContentItem *contentItem `xml:"contentItem"`
|
||||
ContentItemType string `xml:"contentItemType"`
|
||||
CreatedOn string `xml:"createdOn"`
|
||||
LastPlayedAt string `xml:"lastplayedat"`
|
||||
Location string `xml:"location"`
|
||||
Name string `xml:"name"`
|
||||
Source *models.RecentItemParitySource `xml:"source,omitempty"`
|
||||
SourceID string `xml:"sourceid"`
|
||||
UpdatedOn string `xml:"updatedOn"`
|
||||
}
|
||||
|
||||
type contentItem struct {
|
||||
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,omitempty"`
|
||||
}
|
||||
|
||||
func recentToXML(r *models.ServiceRecent) recent {
|
||||
utcTime := int64(0)
|
||||
|
||||
if r.UtcTime != "" {
|
||||
if t, parseErr := strconv.ParseInt(r.UtcTime, 10, 64); parseErr == nil {
|
||||
utcTime = t
|
||||
}
|
||||
}
|
||||
|
||||
createdOn := r.CreatedOn
|
||||
if createdOn == "" {
|
||||
createdOn = FormatTime(time.Now())
|
||||
}
|
||||
|
||||
updatedOn := r.UpdatedOn
|
||||
if updatedOn == "" {
|
||||
updatedOn = createdOn
|
||||
}
|
||||
|
||||
lastPlayedAt := r.LastPlayedAt
|
||||
if lastPlayedAt == "" && utcTime > 0 {
|
||||
lastPlayedAt = time.Unix(utcTime, 0).UTC().Format("2006-01-02T15:04:05.000+00:00")
|
||||
}
|
||||
|
||||
res := recent{
|
||||
ID: r.ID,
|
||||
ContentItemType: r.ContentItemType,
|
||||
CreatedOn: createdOn,
|
||||
UpdatedOn: updatedOn,
|
||||
LastPlayedAt: lastPlayedAt,
|
||||
Location: r.Location,
|
||||
Name: r.Name,
|
||||
SourceID: r.SourceID,
|
||||
ContentItem: &contentItem{
|
||||
Source: r.Source,
|
||||
Type: r.Type,
|
||||
Location: r.Location,
|
||||
SourceAccount: r.SourceAccount,
|
||||
IsPresetable: r.IsPresetable,
|
||||
ItemName: r.Name,
|
||||
ContainerArt: r.ContainerArt,
|
||||
},
|
||||
}
|
||||
|
||||
if r.SourceConfig != nil {
|
||||
res.Source = prepareRecentItemParitySource(r.SourceConfig)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// ProviderSettingsToXML generates provider settings XML for the specified account.
|
||||
func ProviderSettingsToXML(account string) string {
|
||||
return constants.XMLHeader + fmt.Sprintf(`<providerSettings>
|
||||
@@ -679,6 +894,23 @@ func AccountFullToXML(ds *datastore.DataStore, account string) ([]byte, error) {
|
||||
return append([]byte(constants.XMLHeader), data...), nil
|
||||
}
|
||||
|
||||
// RemovePreset clears a preset for the specified account and device.
|
||||
func RemovePreset(ds *datastore.DataStore, account, device string, presetNumber int) error {
|
||||
presets, err := ds.GetPresets(account, device)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if presetNumber < 1 || presetNumber > len(presets) {
|
||||
// Preset doesn't exist or index out of range, nothing to do
|
||||
return nil
|
||||
}
|
||||
|
||||
presets[presetNumber-1] = models.ServicePreset{}
|
||||
|
||||
return ds.SavePresets(account, device, presets)
|
||||
}
|
||||
|
||||
// UpdatePreset updates or creates a preset for the specified account and device.
|
||||
func UpdatePreset(ds *datastore.DataStore, account, device string, presetNumber int, sourceXML []byte) ([]byte, error) {
|
||||
sources, err := ds.GetConfiguredSources(account, device)
|
||||
@@ -760,8 +992,14 @@ func UpdatePreset(ds *datastore.DataStore, account, device string, presetNumber
|
||||
// Return XML for the single preset
|
||||
PrepareConfiguredSource(matchingSrc)
|
||||
presetObj.SourceConfig = matchingSrc
|
||||
presetObj.Username = newPresetElem.Name
|
||||
|
||||
data, err := xml.Marshal(presetObj)
|
||||
// Parity: return the preset wrapped in <presets>
|
||||
px := PresetsXML{
|
||||
Presets: []models.ServicePreset{presetObj},
|
||||
}
|
||||
|
||||
data, err := xml.Marshal(px)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -837,6 +1075,14 @@ func syncMatchingSource(matchingSrc *models.ConfiguredSource, input recentInput)
|
||||
if matchingSrc.DisplayName == "" && matchingSrc.SourceName != "" {
|
||||
matchingSrc.DisplayName = matchingSrc.SourceName
|
||||
}
|
||||
|
||||
if matchingSrc.SourceName == "" && matchingSrc.DisplayName != "" {
|
||||
matchingSrc.SourceName = matchingSrc.DisplayName
|
||||
}
|
||||
|
||||
if matchingSrc.Username == "" && matchingSrc.DisplayName != "" {
|
||||
matchingSrc.Username = matchingSrc.DisplayName
|
||||
}
|
||||
}
|
||||
|
||||
// AddRecent adds or updates a recent item for the specified account and device.
|
||||
@@ -1217,6 +1463,10 @@ func formatRecentResponse(recentObj *models.ServiceRecent, matchingSrc *models.C
|
||||
if res.Source.Credential.Type == "" && matchingSrc.SecretType != "" {
|
||||
res.Source.Credential.Type = matchingSrc.SecretType
|
||||
}
|
||||
|
||||
if res.Source.SourceName == "" {
|
||||
res.Source.SourceName = res.Source.Username
|
||||
}
|
||||
}
|
||||
|
||||
data, _ := xml.MarshalIndent(res, "", " ")
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
### DELETE /streaming/account/{{accountId}}/device/{{deviceId}}/preset/6
|
||||
DELETE {{host}}/streaming/account/{{accountId}}/device/{{deviceId}}/preset/6
|
||||
Host: streaming.bose.com
|
||||
Accept: application/vnd.bose.streaming-v1.2+xml
|
||||
Authorization: Bearer {{token}}
|
||||
Content-Type: application/vnd.bose.streaming-v1.2+xml
|
||||
User-Agent: Bose_Lisa/27.0.6
|
||||
|
||||
> {%
|
||||
client.test("Response is 200 OK", function() {
|
||||
client.assert(response.status === 200, "Response status is not 200");
|
||||
});
|
||||
%}
|
||||
@@ -0,0 +1,65 @@
|
||||
### GET /streaming/account/{{accountId}}/device/{{deviceId}}/presets
|
||||
GET {{host}}/streaming/account/{{accountId}}/device/{{deviceId}}/presets
|
||||
Host: streaming.bose.com
|
||||
Accept: application/vnd.bose.streaming-v1.2+xml
|
||||
Authorization: Bearer {{token}}
|
||||
Content-Type: application/vnd.bose.streaming-v1.2+xml
|
||||
User-Agent: Bose_Lisa/27.0.6
|
||||
|
||||
> {%
|
||||
client.test("Response is 200 OK", function() {
|
||||
client.assert(response.status === 200, "Response status is not 200");
|
||||
});
|
||||
|
||||
client.test("Response body contains presets", function() {
|
||||
const doc = response.body;
|
||||
const presets = doc.getElementsByTagName("presets")[0];
|
||||
client.assert(presets !== null, "Response body does not contain <presets>");
|
||||
|
||||
const presetList = doc.getElementsByTagName("preset");
|
||||
// Based on the flow: set_preset_6, get_presets, delete_preset_6, set_preset_5
|
||||
// The service is fresh, so only what we set is there.
|
||||
client.assert(presetList.length === 1, "Response body should contain exactly one <preset>, but found " + presetList.length);
|
||||
|
||||
const firstPreset = presetList[0];
|
||||
client.assert(firstPreset.getAttribute("buttonNumber") === "6", "First preset should have buttonNumber=\"6\"");
|
||||
|
||||
// Check <name>
|
||||
const name = firstPreset.getElementsByTagName("name")[0];
|
||||
client.assert(name.textContent === "SMOOTH JAZZ", "Preset name should be 'SMOOTH JAZZ'");
|
||||
|
||||
// Check <location>
|
||||
const location = firstPreset.getElementsByTagName("location")[0];
|
||||
client.assert(location.textContent === "/v1/playback/station/s166521", "Preset location mismatch");
|
||||
|
||||
// Check <source> and its attributes/children
|
||||
const source = firstPreset.getElementsByTagName("source")[0];
|
||||
client.assert(source !== null, "Preset should have a <source>");
|
||||
// The source ID is set in HandleMargeUpdatePreset based on matching source, or in UpdatePreset.
|
||||
// For TUNEIN it might be 10004 or similar in our mocks.
|
||||
client.assert(source.getAttribute("id") !== null && source.getAttribute("id") !== "", "Source id should be non-empty");
|
||||
client.assert(source.getAttribute("type") === "Audio", "Source type should be 'Audio'");
|
||||
|
||||
const sourceproviderid = source.getElementsByTagName("sourceproviderid")[0];
|
||||
client.assert(sourceproviderid.textContent === "25", "sourceproviderid should be '25'");
|
||||
|
||||
// Check <username> inside <preset> (not the one in <source> if present)
|
||||
const presetUsernames = firstPreset.getElementsByTagName("username");
|
||||
// In the reference, there's one in <source> (empty) and one in <preset> (SMOOTH JAZZ)
|
||||
// Usually, getElementsByTagName on firstPreset returns all descendants.
|
||||
// Let's be careful about the index or use a more specific selector if possible,
|
||||
// but here we can check the values.
|
||||
let foundPresetUsername = false;
|
||||
for (let i = 0; i < presetUsernames.length; i++) {
|
||||
if (presetUsernames[i].textContent === "SMOOTH JAZZ") {
|
||||
foundPresetUsername = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
client.assert(foundPresetUsername, "Preset should have <username>SMOOTH JAZZ</username>");
|
||||
|
||||
// Ensure <containerArt> is non-empty
|
||||
const containerArt = firstPreset.getElementsByTagName("containerArt")[0];
|
||||
client.assert(containerArt.textContent.startsWith("https://"), "containerArt should be a valid URL");
|
||||
});
|
||||
%}
|
||||
@@ -0,0 +1,57 @@
|
||||
### GET /streaming/account/{{accountId}}/device/{{deviceId}}/recents
|
||||
GET {{host}}/streaming/account/{{accountId}}/device/{{deviceId}}/recents
|
||||
User-Agent: Bose_Lisa/27.0.6
|
||||
Accept: application/vnd.bose.streaming-v1.2+xml
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
> {%
|
||||
client.test("Request executed successfully", function() {
|
||||
client.assert(response.status === 200, "Response status is not 200");
|
||||
});
|
||||
|
||||
client.test("Response content-type is correct", function() {
|
||||
var type = response.contentType.mimeType;
|
||||
client.assert(type === "application/vnd.bose.streaming-v1.2+xml", "Expected 'application/vnd.bose.streaming-v1.2+xml' but received '" + type + "'");
|
||||
});
|
||||
|
||||
client.test("Response body structure", function() {
|
||||
var recents = response.body.getElementsByTagName("recents")[0];
|
||||
client.assert(recents !== null, "Missing 'recents' root element");
|
||||
|
||||
var recentList = recents.getElementsByTagName("recent");
|
||||
client.assert(recentList.length > 0, "No 'recent' elements found");
|
||||
|
||||
var recent = recentList[0];
|
||||
client.assert(recent.getAttribute("id").length > 0, "Missing or empty 'id' attribute on 'recent' element");
|
||||
|
||||
client.assert(recent.getElementsByTagName("contentItemType")[0].textContent.length > 0, "Missing or empty 'contentItemType'");
|
||||
client.assert(recent.getElementsByTagName("createdOn")[0].textContent.length > 0, "Missing or empty 'createdOn'");
|
||||
client.assert(recent.getElementsByTagName("lastplayedat")[0].textContent.length > 0, "Missing or empty 'lastplayedat'");
|
||||
client.assert(recent.getElementsByTagName("location")[0].textContent.length > 0, "Missing or empty 'location'");
|
||||
client.assert(recent.getElementsByTagName("name")[0].textContent.length > 0, "Missing or empty 'name'");
|
||||
|
||||
var source = recent.getElementsByTagName("source")[0];
|
||||
client.assert(source !== null, "Missing 'source' element");
|
||||
client.assert(source.getAttribute("id").length > 0, "Missing or empty 'source' id");
|
||||
client.assert(source.getAttribute("type").length > 0, "Missing or empty 'source' type");
|
||||
|
||||
client.assert(source.getElementsByTagName("createdOn")[0].textContent.length > 0, "Missing or empty source 'createdOn'");
|
||||
|
||||
var credential = source.getElementsByTagName("credential")[0];
|
||||
client.assert(credential.getAttribute("type").length > 0, "Missing or empty credential 'type'");
|
||||
client.assert(credential.textContent.length > 0, "Missing or empty credential value");
|
||||
|
||||
client.assert(source.getElementsByTagName("name")[0] !== null, "Missing source 'name'");
|
||||
client.assert(source.getElementsByTagName("sourceproviderid")[0].textContent.length > 0, "Missing or empty source 'sourceproviderid'");
|
||||
client.assert(source.getElementsByTagName("sourcename")[0].textContent.length > 0, "Missing or empty source 'sourcename'");
|
||||
|
||||
var sourceSettings = source.getElementsByTagName("sourceSettings")[0];
|
||||
client.assert(sourceSettings !== undefined, "Missing 'sourceSettings'");
|
||||
|
||||
client.assert(source.getElementsByTagName("updatedOn")[0].textContent.length > 0, "Missing or empty source 'updatedOn'");
|
||||
client.assert(source.getElementsByTagName("username")[0].textContent.length > 0, "Missing or empty source 'username'");
|
||||
|
||||
client.assert(recent.getElementsByTagName("sourceid")[0].textContent.length > 0, "Missing or empty 'sourceid'");
|
||||
client.assert(recent.getElementsByTagName("updatedOn")[0].textContent.length > 0, "Missing or empty 'updatedOn'");
|
||||
});
|
||||
%}
|
||||
@@ -7,3 +7,13 @@ Authorization: Bearer {{token}}
|
||||
client.assert(response.status === 200, "Response status is not 200");
|
||||
});
|
||||
%}
|
||||
|
||||
### DELETE /streaming/account/{{accountId}}/device/{{deviceId}} (Unregister Device Variant)
|
||||
DELETE {{host}}/streaming/account/{{accountId}}/device/{{deviceId}}
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
> {%
|
||||
client.test("Device unregistered successfully (variant)", function() {
|
||||
client.assert(response.status === 200, "Response status is not 200");
|
||||
});
|
||||
%}
|
||||
|
||||
@@ -35,9 +35,9 @@ Interactions for `20260328-103522-477978/`:
|
||||
| 0031 | mirror | ☑ | GET /streaming/account/{{accountId}}/device/{{device_id}}/group/ | 200 OK | ./mirror/streaming/account/{accountId}/device/{device_id}/group/0031-20260328-103736.093-GET.http |
|
||||
| 0032 | mirror | ☑ | GET /streaming/account/{{accountId}}/provider_settings | 200 OK | ./mirror/streaming/account/{accountId}/provider_settings/0032-20260328-103736.130-GET.http |
|
||||
| 0033 | self | ☑ | POST /oauth/device/{{device_id}}/music/musicprovider/15/token/cs3 | 200 OK | ./self/oauth/device/{device_id}/music/musicprovider/15/token/cs3/0033-20260328-103737.874-POST.http |
|
||||
| 0034 | self | | GET /streaming/account/{{accountId}}/device/{{device_id}}/presets | 200 OK | ./self/streaming/account/{accountId}/device/{device_id}/presets/0034-20260328-103905.297-GET.http |
|
||||
| 0034 | self | ☑ | GET /streaming/account/{{accountId}}/device/{{device_id}}/presets | 200 OK | ./self/streaming/account/{accountId}/device/{device_id}/presets/0034-20260328-103905.297-GET.http |
|
||||
| 0035 | self | ☑ | GET /streaming/software/update/account/{{accountId}} | 200 OK | ./self/streaming/software/update/account/{accountId}/0035-20260328-103905.306-GET.http |
|
||||
| 0036 | mirror | | GET /streaming/account/{{accountId}}/device/{{device_id}}/presets | 200 OK | ./mirror/streaming/account/{accountId}/device/{device_id}/presets/0036-20260328-103905.479-GET.http |
|
||||
| 0036 | mirror | ☑ | GET /streaming/account/{{accountId}}/device/{{device_id}}/presets | 200 OK | ./mirror/streaming/account/{accountId}/device/{device_id}/presets/0036-20260328-103905.479-GET.http |
|
||||
| 0037 | mirror | ☑ | GET /streaming/software/update/account/{{accountId}} | 200 OK | ./mirror/streaming/software/update/account/{accountId}/0037-20260328-103905.669-GET.http |
|
||||
| 0038 | self | ☑ | GET /updates/soundtouch?serialnumber=_serial_ | 200 OK | ./self/updates/soundtouch/0038-20260328-103905.929-GET.http |
|
||||
| 0039 | self | ☑ | GET /streaming/software/update/account/{{accountId}} | 200 OK | ./self/streaming/software/update/account/{accountId}/0039-20260328-104218.080-GET.http |
|
||||
@@ -46,9 +46,9 @@ Interactions for `20260328-103522-477978/`:
|
||||
| 0042 | self | ☑ | GET /streaming/software/update/account/{{accountId}} | 200 OK | ./self/streaming/software/update/account/{accountId}/0042-20260328-104325.629-GET.http |
|
||||
| 0043 | mirror | ☑ | GET /streaming/software/update/account/{{accountId}} | 200 OK | ./mirror/streaming/software/update/account/{accountId}/0043-20260328-104325.814-GET.http |
|
||||
| 0044 | self | ☑ | GET /updates/soundtouch?serialnumber=_serial_ | 200 OK | ./self/updates/soundtouch/0044-20260328-104326.087-GET.http |
|
||||
| 0045 | upstream | | DELETE https://streaming.bose.com/streaming/account/{{accountId}}/device/{{device_id}} | 400 Bad Request | ./upstream/streaming/account/{accountId}/device/{device_id}/0045-20260328-104348.987-DELETE.http |
|
||||
| 0046 | self | | DELETE /streaming/account/{{accountId}}/device/{{device_id}} | 400 Bad Request | ./self/streaming/account/{accountId}/device/{device_id}/0046-20260328-104348.988-DELETE.http |
|
||||
| 0047 | mirror | | DELETE /streaming/account/{{accountId}}/device/{{device_id}} | 200 OK | ./mirror/streaming/account/{accountId}/device/{device_id}/0047-20260328-104348.989-DELETE.http |
|
||||
| 0045 | upstream | ☑ | DELETE https://streaming.bose.com/streaming/account/{{accountId}}/device/{{device_id}} | 400 Bad Request | ./upstream/streaming/account/{accountId}/device/{device_id}/0045-20260328-104348.987-DELETE.http |
|
||||
| 0046 | self | ☑ | DELETE /streaming/account/{{accountId}}/device/{{device_id}} | 400 Bad Request | ./self/streaming/account/{accountId}/device/{device_id}/0046-20260328-104348.988-DELETE.http |
|
||||
| 0047 | mirror | ☑ | DELETE /streaming/account/{{accountId}}/device/{{device_id}} | 200 OK | ./mirror/streaming/account/{accountId}/device/{device_id}/0047-20260328-104348.989-DELETE.http |
|
||||
| 0048 | self | ☑ | POST /streaming/support/power_on | 200 OK | ./self/streaming/support/power_on/0048-20260328-104523.826-POST.http |
|
||||
| 0049 | self | ☑ | GET /bmx/registry/v1/services | 200 OK | ./self/bmx/registry/v1/services/0049-20260328-104523.828-GET.http |
|
||||
| 0050 | mirror | ☑ | POST /streaming/support/power_on | 400 Bad Request | ./mirror/streaming/support/power_on/0050-20260328-104524.002-POST.http |
|
||||
@@ -146,9 +146,9 @@ Interactions for `20260328-103522-477978/`:
|
||||
| 0142 | mirror | ☑ | GET /streaming/account/{{accountId}}/device/{{device_id}}/group/ | 200 OK | ./mirror/streaming/account/{accountId}/device/{device_id}/group/0142-20260329-155216.805-GET.http |
|
||||
| 0143 | self | ☑ | GET /streaming/account/{{accountId}}/device/{{device_id}}/group/ | 200 OK | ./self/streaming/account/{accountId}/device/{device_id}/group/0143-20260329-155221.905-GET.http |
|
||||
| 0144 | mirror | ☑ | GET /streaming/account/{{accountId}}/device/{{device_id}}/group/ | 200 OK | ./mirror/streaming/account/{accountId}/device/{device_id}/group/0144-20260329-155222.082-GET.http |
|
||||
| 0145 | mirror | | GET /streaming/account/{{accountId}}/device/{{device_id}}/recents | 200 OK | ./mirror/streaming/account/{accountId}/device/{device_id}/recents/0145-20260329-185442.017-GET.http |
|
||||
| 0146 | upstream | | GET https://streaming.bose.com/streaming/account/{{accountId}}/device/{{device_id}}/recents | 200 OK | ./upstream/streaming/account/{accountId}/device/{device_id}/recents/0146-20260329-185442.128-GET.http |
|
||||
| 0147 | self | | GET /streaming/account/{{accountId}}/device/{{device_id}}/recents | 200 OK | ./self/streaming/account/{accountId}/device/{device_id}/recents/0147-20260329-185442.130-GET.http |
|
||||
| 0145 | mirror | ☑ | GET /streaming/account/{{accountId}}/device/{{device_id}}/recents | 200 OK | ./mirror/streaming/account/{accountId}/device/{device_id}/recents/0145-20260329-185442.017-GET.http |
|
||||
| 0146 | upstream | ☑ | GET https://streaming.bose.com/streaming/account/{{accountId}}/device/{{device_id}}/recents | 200 OK | ./upstream/streaming/account/{accountId}/device/{device_id}/recents/0146-20260329-185442.128-GET.http |
|
||||
| 0147 | self | ☑ | GET /streaming/account/{{accountId}}/device/{{device_id}}/recents | 200 OK | ./self/streaming/account/{accountId}/device/{device_id}/recents/0147-20260329-185442.130-GET.http |
|
||||
| 0148 | self | ☑ | GET /streaming/account/{{accountId}}/device/{{device_id}}/group/ | 200 OK | ./self/streaming/account/{accountId}/device/{device_id}/group/0148-20260329-193915.264-GET.http |
|
||||
| 0149 | mirror | ☑ | GET /streaming/account/{{accountId}}/device/{{device_id}}/group/ | 200 OK | ./mirror/streaming/account/{accountId}/device/{device_id}/group/0149-20260329-193915.468-GET.http |
|
||||
| 0150 | self | ☑ | GET /streaming/account/{{accountId}}/device/{{device_id}}/group/ | 200 OK | ./self/streaming/account/{accountId}/device/{device_id}/group/0150-20260329-215246.808-GET.http |
|
||||
@@ -201,14 +201,14 @@ Interactions for `20260328-103522-477978/`:
|
||||
| 0197 | upstream | | POST https://content.api.bose.io/bmx/tunein/v1/report?stream_id=e536753726&guide_id=s166521&listen_id=1774819980&stream_type=liveRadio | 200 OK | ./upstream/bmx/tunein/v1/report/0197-20260329-233306.182-POST.http |
|
||||
| 0198 | self | | POST /bmx/tunein/v1/report?stream_id=e536753726&guide_id=s166521&listen_id=1774819980&stream_type=liveRadio | 200 OK | ./self/bmx/tunein/v1/report/0198-20260329-233306.184-POST.http |
|
||||
| 0199 | self | ☑ | GET /streaming/software/update/account/{{accountId}} | 200 OK | ./self/streaming/software/update/account/{accountId}/0199-20260329-233317.196-GET.http |
|
||||
| 0200 | self | | GET /streaming/account/{{accountId}}/device/{{device_id}}/presets | 200 OK | ./self/streaming/account/{accountId}/device/{device_id}/presets/0200-20260329-233317.206-GET.http |
|
||||
| 0201 | mirror | | GET /streaming/account/{{accountId}}/device/{{device_id}}/presets | 200 OK | ./mirror/streaming/account/{accountId}/device/{device_id}/presets/0201-20260329-233317.394-GET.http |
|
||||
| 0200 | self | ☑ | GET /streaming/account/{{accountId}}/device/{{device_id}}/presets | 200 OK | ./self/streaming/account/{accountId}/device/{device_id}/presets/0200-20260329-233317.206-GET.http |
|
||||
| 0201 | mirror | ☑ | GET /streaming/account/{{accountId}}/device/{{device_id}}/presets | 200 OK | ./mirror/streaming/account/{accountId}/device/{device_id}/presets/0201-20260329-233317.394-GET.http |
|
||||
| 0202 | mirror | ☑ | GET /streaming/software/update/account/{{accountId}} | 200 OK | ./mirror/streaming/software/update/account/{accountId}/0202-20260329-233317.409-GET.http |
|
||||
| 0203 | self | ☑ | GET /updates/soundtouch?serialnumber=_serial_ | 200 OK | ./self/updates/soundtouch/0203-20260329-233317.838-GET.http |
|
||||
| 0204 | self | ☑ | POST /v1/scmudc/{{device_id}} | 200 OK | ./self/v1/scmudc/{device_id}/0204-20260329-233330.871-POST.http |
|
||||
| 0205 | mirror | | PUT /streaming/account/{{accountId}}/device/{{device_id}}/preset/6 | 200 OK | ./mirror/streaming/account/{accountId}/device/{device_id}/preset/6/0205-20260329-233331.093-PUT.http |
|
||||
| 0206 | upstream | | PUT https://streaming.bose.com/streaming/account/{{accountId}}/device/{{device_id}}/preset/6 | 500 Internal Server Error | ./upstream/streaming/account/{accountId}/device/{device_id}/preset/6/0206-20260329-233331.096-PUT.http |
|
||||
| 0207 | self | | PUT /streaming/account/{{accountId}}/device/{{device_id}}/preset/6 | 500 Internal Server Error | ./self/streaming/account/{accountId}/device/{device_id}/preset/6/0207-20260329-233331.096-PUT.http |
|
||||
| 0205 | mirror | ☑ | PUT /streaming/account/{{accountId}}/device/{{device_id}}/preset/6 | 200 OK | ./mirror/streaming/account/{accountId}/device/{device_id}/preset/6/0205-20260329-233331.093-PUT.http |
|
||||
| 0206 | upstream | ☑ | PUT https://streaming.bose.com/streaming/account/{{accountId}}/device/{{device_id}}/preset/6 | 500 Internal Server Error | ./upstream/streaming/account/{accountId}/device/{device_id}/preset/6/0206-20260329-233331.096-PUT.http |
|
||||
| 0207 | self | ☑ | PUT /streaming/account/{{accountId}}/device/{{device_id}}/preset/6 | 500 Internal Server Error | ./self/streaming/account/{accountId}/device/{device_id}/preset/6/0207-20260329-233331.096-PUT.http |
|
||||
| 0208 | mirror | ☑ | POST /v1/scmudc/{{device_id}} | 200 OK | ./mirror/v1/scmudc/{device_id}/0208-20260329-233331.323-POST.http |
|
||||
| 0209 | self | ☑ | POST /v1/scmudc/{{device_id}} | 200 OK | ./self/v1/scmudc/{device_id}/0209-20260329-233331.938-POST.http |
|
||||
| 0210 | self | ☑ | POST /v1/scmudc/{{device_id}} | 200 OK | ./self/v1/scmudc/{device_id}/0210-20260329-233331.982-POST.http |
|
||||
|
||||
Reference in New Issue
Block a user