refactor(speaker): introduce pkg/speaker leaf for shared protocol constants

The HTTP port and on-device paths for the SoundTouch speaker were
duplicated across pkg/client (unexported) and pkg/service/constants
(under a service-layer prefix). Both spots needed the same values, and
the next round of work (group/persistence handling in the CLI) would
have created a third — or worse, dragged pkg/service into the CLI's
dependency graph just for a port number.

pkg/speaker is a no-deps leaf that holds the speaker-protocol
constants: HTTPPort, the request paths, and the on-device persistence
file locations (now including GroupServiceFileLocation, for the
upcoming stereo-pair sync work). The client library, the service, the
CLI, and tests can all import it without introducing a layering edge.

This commit moves nothing into pkg/speaker that doesn't belong there —
the service-specific constants (provider IDs, file names, date stub,
etc.) stay in pkg/service/constants. Only the genuinely
protocol-level values move.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tobias Gesellchen
2026-05-11 23:18:08 +02:00
co-authored by Claude Opus 4.7
parent bb71253690
commit c8c38b78e6
6 changed files with 67 additions and 15 deletions
+4 -3
View File
@@ -6,6 +6,7 @@ import (
"time"
"github.com/gesellix/bose-soundtouch/pkg/models"
"github.com/gesellix/bose-soundtouch/pkg/speaker"
)
// Integration tests for bass control functionality
@@ -426,7 +427,7 @@ func BenchmarkClient_Bass_Integration(b *testing.B) {
// This is a simple version for test use
func parseBassHostPort(hostPort string) (string, int) {
if !containsSubstring(hostPort, ":") {
return hostPort, defaultSoundTouchPort
return hostPort, speaker.HTTPPort
}
// Simple parsing - in real use, we'd use net.SplitHostPort
@@ -448,7 +449,7 @@ func parseBassHostPort(hostPort string) (string, int) {
if len(parts) == 2 {
// Try to parse port
port := defaultSoundTouchPort
port := speaker.HTTPPort
portStr := parts[1]
portInt := 0
@@ -468,5 +469,5 @@ func parseBassHostPort(hostPort string) (string, int) {
return parts[0], port
}
return hostPort, defaultSoundTouchPort
return hostPort, speaker.HTTPPort
}
@@ -4,6 +4,8 @@ import (
"os"
"testing"
"time"
"github.com/gesellix/bose-soundtouch/pkg/speaker"
)
// Integration tests for source selection functionality
@@ -386,7 +388,7 @@ func BenchmarkClient_SelectSource_Integration(b *testing.B) {
// This is a simple version for test use
func parseHostPort(hostPort string) (string, int) {
if !containsSubstring(hostPort, ":") {
return hostPort, defaultSoundTouchPort
return hostPort, speaker.HTTPPort
}
// Simple parsing - in real use, we'd use net.SplitHostPort
@@ -408,7 +410,7 @@ func parseHostPort(hostPort string) (string, int) {
if len(parts) == 2 {
// Try to parse port
port := defaultSoundTouchPort
port := speaker.HTTPPort
portStr := parts[1]
portInt := 0
@@ -428,5 +430,5 @@ func parseHostPort(hostPort string) (string, int) {
return parts[0], port
}
return hostPort, defaultSoundTouchPort
return hostPort, speaker.HTTPPort
}
+4 -5
View File
@@ -299,11 +299,10 @@ const (
RecentsFile = "Recents.xml"
SourcesFile = "Sources.xml"
SpeakerHTTPPort = 8090
SpeakerDeviceInfoPath = "/info"
SpeakerRecentsPath = "/recents"
SpeakerPresetsPath = "/presets"
SpeakerSourcesFileLocation = "/mnt/nv/BoseApp-Persistence/1/Sources.xml"
// Speaker-protocol constants (HTTP port, paths, on-device file
// locations) moved to github.com/gesellix/bose-soundtouch/pkg/speaker
// so the client library and CLI can share them without depending on
// the service package.
// DateStr is the hardcoded date used in many Bose XML responses
DateStr = "2012-09-19T12:43:00.000+00:00"
-4
View File
@@ -9,10 +9,6 @@ func TestConstants(t *testing.T) {
t.Error("DateStr should not be empty")
}
if SpeakerHTTPPort != 8090 {
t.Errorf("Expected SpeakerHTTPPort 8090, got %d", SpeakerHTTPPort)
}
if len(GetProviders()) == 0 {
t.Error("Providers should not be empty")
}
+29
View File
@@ -0,0 +1,29 @@
// Package speaker holds protocol-level constants for the Bose SoundTouch
// speaker's local API surface: the well-known HTTP port, the request paths
// exposed by every device, and the on-device file locations the migration
// flow needs to know about.
//
// This package is intentionally a leaf with no internal dependencies, so
// any layer can import it (client library, service, CLI, tests) without
// introducing a cycle or a cross-topic edge. Anything speaker-shaped that
// would otherwise be duplicated between packages belongs here.
package speaker
// HTTPPort is the well-known port the SoundTouch device exposes its local
// API on (e.g. /info, /presets, /group).
const HTTPPort = 8090
// Well-known HTTP paths the SoundTouch device serves on HTTPPort.
const (
DeviceInfoPath = "/info"
PresetsPath = "/presets"
RecentsPath = "/recents"
)
// On-device filesystem paths that the migration/sync flow needs to read or
// write over SSH. These live in the device's persistence area and are not
// part of the HTTP surface.
const (
SourcesFileLocation = "/mnt/nv/BoseApp-Persistence/1/Sources.xml"
GroupServiceFileLocation = "/mnt/nv/BoseApp-Persistence/1/GroupService.xml"
)
+25
View File
@@ -0,0 +1,25 @@
package speaker
import "testing"
// Sanity-check the well-known values — a wrong number here would silently
// break every transport and is cheap to guard against.
func TestSpeakerConstants(t *testing.T) {
if HTTPPort != 8090 {
t.Errorf("HTTPPort = %d, want 8090", HTTPPort)
}
cases := map[string]string{
"DeviceInfoPath": DeviceInfoPath,
"PresetsPath": PresetsPath,
"RecentsPath": RecentsPath,
"SourcesFileLocation": SourcesFileLocation,
"GroupServiceFileLocation": GroupServiceFileLocation,
}
for name, val := range cases {
if val == "" {
t.Errorf("%s is empty", name)
}
}
}