diff --git a/Makefile b/Makefile
index af71d6b..5cd5f3e 100644
--- a/Makefile
+++ b/Makefile
@@ -111,6 +111,9 @@ test-http-client:
@docker build -t soundtouch-service-test .
@docker run -d --name soundtouch-service --network soundtouch-test-net \
-e PORT=8000 \
+ -e SPOTIFY_CLIENT_ID=mock-id \
+ -e SPOTIFY_CLIENT_SECRET=mock-secret \
+ -v $(PWD)/tests/integration/testdata:/app/data \
soundtouch-service-test
@echo "Waiting for service to start..."
@sleep 5
@@ -128,10 +131,12 @@ test-http-client:
/workdir/get_software_update.http \
/workdir/get_soundtouch_updates.http \
/workdir/get_streaming_token.http \
+ /workdir/post_oauth_token.http \
/workdir/get_provider_settings.http \
/workdir/tunein_playback_station.http \
/workdir/set_preset_6.http \
/workdir/set_preset_5.http \
+ /workdir/post_recent.http \
/workdir/get_full_account.http \
/workdir/get_group.http \
/workdir/unregister_device.http \
diff --git a/cmd/soundtouch-service/main.go b/cmd/soundtouch-service/main.go
index 8846e92..62df1fa 100644
--- a/cmd/soundtouch-service/main.go
+++ b/cmd/soundtouch-service/main.go
@@ -63,8 +63,40 @@ func initializeDefaultSources(ds *datastore.DataStore) {
if sources, errGet := ds.GetConfiguredSources(dev.AccountID, dev.DeviceID); errGet == nil {
log.Printf("Initializing default Sources.xml for existing device %s", dev.DeviceID)
- if errSave := ds.SaveConfiguredSources(dev.AccountID, dev.DeviceID, sources); errSave != nil {
- log.Printf("Failed to save default sources for %s: %v", dev.DeviceID, errSave)
+ // Find default sources and merge them if missing or outdated tokens
+ defaults := ds.GetDefaultSources()
+ modified := false
+
+ for i := range defaults {
+ def := defaults[i]
+ found := false
+
+ for j := range sources {
+ if sources[j].SourceKeyType == def.SourceKeyType {
+ found = true
+
+ if sources[j].Secret == "" && def.Secret != "" {
+ log.Printf("Initializing missing token for source %s on device %s", def.SourceKeyType, dev.DeviceID)
+ sources[j].Secret = def.Secret
+ sources[j].SecretType = def.SecretType
+ modified = true
+ }
+
+ break
+ }
+ }
+
+ if !found {
+ log.Printf("Adding missing default source %s to device %s", def.SourceKeyType, dev.DeviceID)
+ sources = append(sources, def)
+ modified = true
+ }
+ }
+
+ if modified {
+ if errSave := ds.SaveConfiguredSources(dev.AccountID, dev.DeviceID, sources); errSave != nil {
+ log.Printf("Failed to save updated sources for %s: %v", dev.DeviceID, errSave)
+ }
}
}
}
diff --git a/pkg/models/models.go b/pkg/models/models.go
index 84e9e72..11e3339 100644
--- a/pkg/models/models.go
+++ b/pkg/models/models.go
@@ -229,10 +229,6 @@ type RecentItemParity struct {
Source *RecentItemParitySource `xml:"source,omitempty"`
SourceID string `xml:"sourceid"`
UpdatedOn string `xml:"updatedOn"`
- Username string `xml:"username"`
- ContainerArt string `xml:"containerArt"`
- SourceAccount string `xml:"sourceAccount"`
- IsPresetable string `xml:"isPresetable"`
}
// RecentItemParitySource represents the source in a RecentItemParity.
@@ -240,7 +236,7 @@ type RecentItemParitySource struct {
ID string `xml:"id,attr"`
Type string `xml:"type,attr"`
CreatedOn string `xml:"createdOn"`
- Credential *RecentItemParityCredential `xml:"credential,omitempty"`
+ Credential *RecentItemParityCredential `xml:"credential"`
Name string `xml:"name"`
SourceProviderID string `xml:"sourceproviderid"`
SourceName string `xml:"sourcename"`
diff --git a/pkg/models/service_recent_parity_test.go b/pkg/models/service_recent_parity_test.go
index 6319a37..2a7bb84 100644
--- a/pkg/models/service_recent_parity_test.go
+++ b/pkg/models/service_recent_parity_test.go
@@ -120,6 +120,14 @@ func TestServiceRecent_Parity(t *testing.T) {
CreatedOn: "2026-03-22T10:00:04.000+00:00",
UpdatedOn: "2026-03-22T10:53:50.719+00:00",
LastPlayedAt: "2026-03-22T10:53:48.000+00:00",
+ Source: &RecentItemParitySource{
+ ID: "10863533",
+ Type: "Audio",
+ Credential: &RecentItemParityCredential{
+ Type: "token",
+ Value: "",
+ },
+ },
}
data, err := xml.MarshalIndent(recent, "", " ")
@@ -137,6 +145,7 @@ func TestServiceRecent_Parity(t *testing.T) {
`Dopamine`,
`10863533`,
`2026-03-22T10:53:50.719+00:00`,
+ ``,
}
for _, expected := range expectedElements {
diff --git a/pkg/service/datastore/datastore.go b/pkg/service/datastore/datastore.go
index 926481d..0b9621e 100644
--- a/pkg/service/datastore/datastore.go
+++ b/pkg/service/datastore/datastore.go
@@ -1180,6 +1180,11 @@ func GenerateSerialSecret(serial string) string {
return base64.StdEncoding.EncodeToString(b)
}
+// GetDefaultSources returns the list of default sources.
+func (ds *DataStore) GetDefaultSources() []models.ConfiguredSource {
+ return ds.getDefaultSources()
+}
+
func (ds *DataStore) getDefaultSources() []models.ConfiguredSource {
sources := []models.ConfiguredSource{
{
diff --git a/pkg/service/handlers/parity_mismatch_repro_test.go b/pkg/service/handlers/parity_mismatch_repro_test.go
index b0278cb..aa1ac78 100644
--- a/pkg/service/handlers/parity_mismatch_repro_test.go
+++ b/pkg/service/handlers/parity_mismatch_repro_test.go
@@ -90,11 +90,6 @@ func TestParityMismatchReproduction_New(t *testing.T) {
if !strings.Contains(bodyStr, `2017-07-20T16:43:48.000+00:00`) {
t.Errorf("Source CreatedOn was not learned from POST in element. Body: %s", bodyStr)
}
-
- // 7. sourceAccount should be present (parity)
- if !strings.Contains(bodyStr, ``) {
- t.Errorf("Missing in flat response. Body: %s", bodyStr)
- }
})
t.Run("Subsequent GET /recents should also show learned source details", func(t *testing.T) {
diff --git a/pkg/service/handlers/recent_parity_test.go b/pkg/service/handlers/recent_parity_test.go
index 9d02f8a..8bb4eda 100644
--- a/pkg/service/handlers/recent_parity_test.go
+++ b/pkg/service/handlers/recent_parity_test.go
@@ -12,6 +12,7 @@ import (
"testing"
"time"
+ "github.com/gesellix/bose-soundtouch/pkg/models"
"github.com/gesellix/bose-soundtouch/pkg/service/datastore"
)
@@ -36,13 +37,32 @@ func TestMargeRecentConsistencyAndIDParity(t *testing.T) {
t.Run("POST recent creates consistent IDs and persists unknown sources", func(t *testing.T) {
payload := `
- tracklisturl
- 2026-03-14T21:33:22.000+00:00
- /playback/container/c3BvdGlmeTphbGJ1bTo3RjUwdWg3b0dpdG1BRVNjUktWNnBE
- Terminal Caribe
- 10863533
+ stationurl
+ 2026-03-29T21:33:00+00:00
+ /v1/playback/station/s166521
+ SMOOTH JAZZ
+ 14774275
`
+ expectedToken := datastore.GenerateSerialSecret("tunein")
+ // Pre-configure source 14774275 as TUNEIN (ID 25)
+ ds.SaveConfiguredSources(account, deviceID, []models.ConfiguredSource{
+ {
+ ID: "14774275",
+ SourceProviderID: "25",
+ Type: "Audio",
+ DisplayName: "TuneIn",
+ Secret: expectedToken,
+ SecretType: "token",
+ SourceKey: struct {
+ Type string `xml:"type,attr"`
+ Account string `xml:"account,attr"`
+ }{
+ Type: "TUNEIN",
+ },
+ },
+ })
+
// 1. POST /recent
res, err := http.Post(ts.URL+"/streaming/account/"+account+"/device/"+deviceID+"/recent", "application/xml", strings.NewReader(payload))
if err != nil {
@@ -50,13 +70,21 @@ func TestMargeRecentConsistencyAndIDParity(t *testing.T) {
}
defer res.Body.Close()
- if res.StatusCode != http.StatusCreated {
- t.Fatalf("Expected status 201, got %d", res.StatusCode)
- }
-
postBody, _ := io.ReadAll(res.Body)
postBodyStr := string(postBody)
+ if res.StatusCode != http.StatusCreated {
+ t.Fatalf("Expected status 201, got %d. Body: %s", res.StatusCode, postBodyStr)
+ }
+
+ // Verify constant token for TUNEIN
+ if !strings.Contains(postBodyStr, expectedToken) {
+ t.Errorf("Response missing expected constant token for TuneIn. Body: %s", postBodyStr)
+ }
+ if !strings.Contains(postBodyStr, ``) {
+ t.Errorf("Response missing expected credential tag for TuneIn. Body: %s", postBodyStr)
+ }
+
// Verify ID format: YYMMDDXXX (9 digits)
// Today's prefix:
prefix := time.Now().UTC().Format("060102")
@@ -92,31 +120,28 @@ func TestMargeRecentConsistencyAndIDParity(t *testing.T) {
if !strings.Contains(getRecentsStr, `id="`+recentID+`"`) {
t.Errorf("GET /recents missing ID %s. Body: %s", recentID, getRecentsStr)
}
- if !strings.Contains(getRecentsStr, `Terminal Caribe`) {
- t.Errorf("GET /recents missing Name 'Terminal Caribe'. Body: %s", getRecentsStr)
+ if !strings.Contains(getRecentsStr, `SMOOTH JAZZ`) {
+ t.Errorf("GET /recents missing Name 'SMOOTH JAZZ'. Body: %s", getRecentsStr)
}
- if !strings.Contains(getRecentsStr, `Terminal Caribe`) {
+ if !strings.Contains(getRecentsStr, `SMOOTH JAZZ`) {
t.Errorf("GET /recents should use nested for ServiceRecent. Body: %s", getRecentsStr)
}
// 4. Verify source persistence
- // Check if source 10863533 was learned and is now in Sources.xml
+ // Check if source 14774275 was learned and is now in Sources.xml
sources, err := ds.GetConfiguredSources(account, deviceID)
if err != nil {
t.Errorf("Failed to get configured sources: %v", err)
}
found := false
for _, s := range sources {
- if s.ID == "10863533" {
+ if s.ID == "14774275" {
found = true
- if s.SourceKeyType != "SPOTIFY" {
- t.Errorf("Learned source should be SPOTIFY based on location, got %s", s.SourceKeyType)
- }
break
}
}
if !found {
- t.Errorf("Source 10863533 was not learned and persisted")
+ t.Errorf("Source 14774275 was not learned and persisted")
}
})
}
diff --git a/pkg/service/marge/marge.go b/pkg/service/marge/marge.go
index 478f2c5..f3e6864 100644
--- a/pkg/service/marge/marge.go
+++ b/pkg/service/marge/marge.go
@@ -119,6 +119,14 @@ func PrepareConfiguredSource(s *models.ConfiguredSource) {
}
}
+ if s.Credential.Type == "" {
+ s.Credential.Type = s.SecretType
+ }
+
+ if s.Credential.Value == "" {
+ s.Credential.Value = s.Secret
+ }
+
// Ensure SourceKey fields are synced with legacy fields if they were used
if s.SourceKey.Type == "" && s.SourceKeyType != "" {
s.SourceKey.Type = s.SourceKeyType
@@ -850,17 +858,30 @@ func AddRecent(ds *datastore.DataStore, account, device string, sourceXML []byte
sourceName := getSourceNameFromXML(sourceXML, input)
+ // 1. learnSource handles persistence AND returns a matching source.
matchingSrc, learned := learnSource(ds, account, device, sources, input.SourceID, input.Location, sourceName, input.Source.Credential.Value, input.Source.SourceProviderID, input.Source.CreatedOn, input.Source.UpdatedOn)
+
+ // Parity: ensure generic tokens for TUNEIN and LOCAL_INTERNET_RADIO if missing.
+ // This covers both learned and already existing sources.
+ if (matchingSrc.SourceProviderID == "25" || matchingSrc.ID == "TUNEIN" || strings.Contains(input.Location, "/v1/playback/station/")) && matchingSrc.Secret == "" {
+ matchingSrc.Secret = datastore.GenerateSerialSecret("tunein")
+ matchingSrc.SecretType = "token"
+ } else if matchingSrc.ID == "LOCAL_INTERNET_RADIO" && matchingSrc.Secret == "" {
+ matchingSrc.Secret = datastore.GenerateSerialSecret("local-internet-radio")
+ matchingSrc.SecretType = "token"
+ }
+
if learned {
- // Re-fetch sources to ensure we have the newly learned one
+ // Re-fetch sources to ensure we have the newly learned one in the slice
if updatedSources, err := ds.GetConfiguredSources(account, device); err == nil {
sources = updatedSources
+ _ = sources // avoid ineffectual assignment
}
-
- matchingSrc = findMatchingSource(sources, input.SourceID)
}
if matchingSrc == nil {
+ // This should technically not happen as learnSource always returns something,
+ // but we keep it as a fallback for safety.
matchingSrc = &models.ConfiguredSource{
ID: input.SourceID,
SourceProviderID: input.Source.SourceProviderID,
@@ -895,6 +916,15 @@ func learnSource(ds *datastore.DataStore, account, device string, sources []mode
}
if sourceLearned {
+ // Ensure generic tokens for TUNEIN and LOCAL_INTERNET_RADIO if missing
+ if (matchingSrc.SourceProviderID == "25" || matchingSrc.ID == "TUNEIN" || strings.Contains(location, "/v1/playback/station/")) && matchingSrc.Secret == "" {
+ matchingSrc.Secret = datastore.GenerateSerialSecret("tunein")
+ matchingSrc.SecretType = "token"
+ } else if matchingSrc.ID == "LOCAL_INTERNET_RADIO" && matchingSrc.Secret == "" {
+ matchingSrc.Secret = datastore.GenerateSerialSecret("local-internet-radio")
+ matchingSrc.SecretType = "token"
+ }
+
persistLearnedSource(ds, account, device, sources, matchingSrc)
}
@@ -929,9 +959,26 @@ func createLearnedSource(sourceID, location, sourceName, credentialValue, source
src.Type = "Audio"
src.SecretType = "token"
+ if src.Secret == "" {
+ src.Secret = datastore.GenerateSerialSecret("tunein")
+ }
+
if src.DisplayName == "Other" || src.DisplayName == "TuneIn" || src.DisplayName == "" {
src.DisplayName = "TuneIn"
}
+ case sourceID == "LOCAL_INTERNET_RADIO":
+ src.SourceKey.Type = "LOCAL_INTERNET_RADIO"
+ src.SourceKeyType = "LOCAL_INTERNET_RADIO"
+ src.Type = "Audio"
+ src.SecretType = "token"
+
+ if src.Secret == "" {
+ src.Secret = datastore.GenerateSerialSecret("local-internet-radio")
+ }
+
+ if src.DisplayName == "Other" || src.DisplayName == "Local Internet Radio" || src.DisplayName == "" {
+ src.DisplayName = "Local Internet Radio"
+ }
case strings.Contains(location, "spotify") || strings.Contains(location, "c3BvdGlme") || sourceID == "SPOTIFY":
src.SourceKey.Type = "SPOTIFY"
src.SourceKeyType = "SPOTIFY"
@@ -1117,17 +1164,15 @@ func formatRecentResponse(recentObj *models.ServiceRecent, matchingSrc *models.C
ID: recentObj.ID,
ContentItemType: recentObj.ContentItemType,
CreatedOn: createdOn,
- UpdatedOn: createdOn,
+ UpdatedOn: recentObj.UpdatedOn,
LastPlayedAt: time.Unix(utcTime, 0).UTC().Format("2006-01-02T15:04:05.000+00:00"),
Location: recentObj.Location,
Name: recentObj.Name,
SourceID: recentObj.SourceID,
- SourceAccount: recentObj.SourceAccount,
- IsPresetable: recentObj.IsPresetable,
}
- if res.SourceAccount == "" {
- res.SourceAccount = "" // Ensure it's not nil if it was a pointer, but it's a string.
+ if res.UpdatedOn == "" {
+ res.UpdatedOn = createdOn
}
if matchingSrc != nil {
@@ -1143,16 +1188,34 @@ func formatRecentResponse(recentObj *models.ServiceRecent, matchingSrc *models.C
Username: matchingSrc.Username,
}
- if matchingSrc.Secret != "" {
+ if res.Source.Name == "TuneIn" || res.Source.Name == "LOCAL_INTERNET_RADIO" {
+ res.Source.Name = ""
+ }
+
+ switch {
+ case matchingSrc.Secret != "":
res.Source.Credential = &models.RecentItemParityCredential{
Type: matchingSrc.SecretType,
Value: matchingSrc.Secret,
}
- } else if matchingSrc.Credential.Value != "" {
+ case matchingSrc.Credential.Value != "":
res.Source.Credential = &models.RecentItemParityCredential{
Type: matchingSrc.Credential.Type,
Value: matchingSrc.Credential.Value,
}
+ default:
+ res.Source.Credential = &models.RecentItemParityCredential{
+ Type: "token",
+ Value: "",
+ }
+ }
+
+ if res.Source.Credential.Value == "" && matchingSrc.Secret != "" {
+ res.Source.Credential.Value = matchingSrc.Secret
+ }
+
+ if res.Source.Credential.Type == "" && matchingSrc.SecretType != "" {
+ res.Source.Credential.Type = matchingSrc.SecretType
}
}
diff --git a/tests/.gitignore b/tests/.gitignore
index f589fcf..59a82cd 100644
--- a/tests/.gitignore
+++ b/tests/.gitignore
@@ -1,2 +1,4 @@
2026*/
data/
+integration/testdata/
+!integration/testdata/spotify/accounts.json
diff --git a/tests/integration/http-client/post_oauth_token.http b/tests/integration/http-client/post_oauth_token.http
index 54b1b50..871e501 100644
--- a/tests/integration/http-client/post_oauth_token.http
+++ b/tests/integration/http-client/post_oauth_token.http
@@ -23,7 +23,7 @@ Content-Type: application/json
client.test("Response body has expected structure", function() {
client.assert(response.body.hasOwnProperty("access_token"), "Response body missing 'access_token'");
- client.assert(response.body.access_token.length > 0, "access_token is empty");
+ client.assert(response.body.access_token === "mock-access-token", "access_token is not 'mock-access-token'");
client.assert(response.body.hasOwnProperty("expires_in"), "Response body missing 'expires_in'");
client.assert(typeof response.body.expires_in === "number", "expires_in is not a number");
diff --git a/tests/integration/testdata/spotify/accounts.json b/tests/integration/testdata/spotify/accounts.json
new file mode 100644
index 0000000..08e7002
--- /dev/null
+++ b/tests/integration/testdata/spotify/accounts.json
@@ -0,0 +1,10 @@
+{
+ "test-user": {
+ "user_id": "test-user",
+ "display_name": "Integration Test User",
+ "email": "test@example.com",
+ "access_token": "mock-access-token",
+ "refresh_token": "mock-refresh-token",
+ "expires_at": 2147483647
+ }
+}
diff --git a/tests/interactions_20260328-103522-477978.md b/tests/interactions_20260328-103522-477978.md
index e672807..23124a6 100644
--- a/tests/interactions_20260328-103522-477978.md
+++ b/tests/interactions_20260328-103522-477978.md
@@ -34,7 +34,7 @@ Interactions for `20260328-103522-477978/`:
| 0030 | mirror | ☑ | GET /streaming/account/{{accountId}}/provider_settings | 200 OK | ./mirror/streaming/account/{accountId}/provider_settings/0030-20260328-103736.065-GET.http |
| 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 |
+| 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 |
| 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 |
@@ -110,7 +110,7 @@ Interactions for `20260328-103522-477978/`:
| 0106 | mirror | ☑ | GET /streaming/account/{{accountId}}/provider_settings | 200 OK | ./mirror/streaming/account/{accountId}/provider_settings/0106-20260328-105554.347-GET.http |
| 0107 | mirror | ☑ | GET /streaming/account/{{accountId}}/provider_settings | 200 OK | ./mirror/streaming/account/{accountId}/provider_settings/0107-20260328-105554.395-GET.http |
| 0108 | mirror | ☑ | GET /streaming/account/{{accountId}}/device/{{device_id}}/group/ | 200 OK | ./mirror/streaming/account/{accountId}/device/{device_id}/group/0108-20260328-105554.406-GET.http |
-| 0109 | self | | POST /oauth/device/{{device_id}}/music/musicprovider/15/token/cs3 | 200 OK | ./self/oauth/device/{device_id}/music/musicprovider/15/token/cs3/0109-20260328-105556.138-POST.http |
+| 0109 | self | ☑ | POST /oauth/device/{{device_id}}/music/musicprovider/15/token/cs3 | 200 OK | ./self/oauth/device/{device_id}/music/musicprovider/15/token/cs3/0109-20260328-105556.138-POST.http |
| 0110 | self | ☑ | GET /streaming/software/update/account/{{accountId}} | 200 OK | ./self/streaming/software/update/account/{accountId}/0110-20260328-105606.529-GET.http |
| 0111 | mirror | ☑ | GET /streaming/software/update/account/{{accountId}} | 200 OK | ./mirror/streaming/software/update/account/{accountId}/0111-20260328-105606.699-GET.http |
| 0112 | self | ☑ | GET /updates/soundtouch?serialnumber=_serial_ | 200 OK | ./self/updates/soundtouch/0112-20260328-105606.961-GET.http |
@@ -186,11 +186,11 @@ Interactions for `20260328-103522-477978/`:
| 0182 | self | ☑ | POST /v1/scmudc/{{device_id}} | 200 OK | ./self/v1/scmudc/{device_id}/0182-20260329-233302.393-POST.http |
| 0183 | self | ☑ | POST /v1/scmudc/{{device_id}} | 200 OK | ./self/v1/scmudc/{device_id}/0183-20260329-233302.636-POST.http |
| 0184 | self | ☑ | POST /v1/scmudc/{{device_id}} | 200 OK | ./self/v1/scmudc/{device_id}/0184-20260329-233302.655-POST.http |
-| 0185 | self | | POST /streaming/account/{{accountId}}/device/{{device_id}}/recent | 201 Created | ./self/streaming/account/{accountId}/device/{device_id}/recent/0185-20260329-233302.671-POST.http |
+| 0185 | self | ☑ | POST /streaming/account/{{accountId}}/device/{{device_id}}/recent | 201 Created | ./self/streaming/account/{accountId}/device/{device_id}/recent/0185-20260329-233302.671-POST.http |
| 0186 | mirror | ☑ | POST /v1/scmudc/{{device_id}} | 200 OK | ./mirror/v1/scmudc/{device_id}/0186-20260329-233302.906-POST.http |
| 0187 | mirror | ☑ | POST /v1/scmudc/{{device_id}} | 200 OK | ./mirror/v1/scmudc/{device_id}/0187-20260329-233303.115-POST.http |
| 0188 | mirror | ☑ | POST /v1/scmudc/{{device_id}} | 200 OK | ./mirror/v1/scmudc/{device_id}/0188-20260329-233303.118-POST.http |
-| 0189 | mirror | | POST /streaming/account/{{accountId}}/device/{{device_id}}/recent | 201 Created | ./mirror/streaming/account/{accountId}/device/{device_id}/recent/0189-20260329-233303.759-POST.http |
+| 0189 | mirror | ☑ | POST /streaming/account/{{accountId}}/device/{{device_id}}/recent | 201 Created | ./mirror/streaming/account/{accountId}/device/{device_id}/recent/0189-20260329-233303.759-POST.http |
| 0190 | self | ☑ | POST /v1/scmudc/{{device_id}} | 200 OK | ./self/v1/scmudc/{device_id}/0190-20260329-233304.143-POST.http |
| 0191 | self | ☑ | POST /v1/scmudc/{{device_id}} | 200 OK | ./self/v1/scmudc/{device_id}/0191-20260329-233304.269-POST.http |
| 0192 | mirror | ☑ | POST /v1/scmudc/{{device_id}} | 200 OK | ./mirror/v1/scmudc/{device_id}/0192-20260329-233304.554-POST.http |
@@ -246,4 +246,4 @@ Interactions for `20260328-103522-477978/`:
| 0242 | self | ☑ | GET /streaming/account/{{accountId}}/provider_settings | 200 OK | ./self/streaming/account/{accountId}/provider_settings/0242-20260329-233508.866-GET.http |
| 0243 | mirror | ☑ | GET /streaming/account/{{accountId}}/provider_settings | 200 OK | ./mirror/streaming/account/{accountId}/provider_settings/0243-20260329-233508.968-GET.http |
| 0244 | mirror | ☑ | GET /streaming/account/{{accountId}}/provider_settings | 200 OK | ./mirror/streaming/account/{accountId}/provider_settings/0244-20260329-233509.041-GET.http |
-| 0245 | self | | POST /oauth/device/{{device_id}}/music/musicprovider/15/token/cs3 | 200 OK | ./self/oauth/device/{device_id}/music/musicprovider/15/token/cs3/0245-20260329-233510.807-POST.http |
+| 0245 | self | ☑ | POST /oauth/device/{{device_id}}/music/musicprovider/15/token/cs3 | 200 OK | ./self/oauth/device/{device_id}/music/musicprovider/15/token/cs3/0245-20260329-233510.807-POST.http |