From a19d34b55e8f3c48dbc4f151020f4ba116985d32 Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Sat, 14 Feb 2026 12:52:48 +0100 Subject: [PATCH] Align soundtouch-service CLI with soundtouch-cli --- cmd/soundtouch-cli/common.go | 3 +- cmd/soundtouch-cli/main.go | 2 +- cmd/soundtouch-service/main.go | 319 +++++++++++++++++++++------------ docs/SOUNDTOUCH-SERVICE.md | 25 +-- 4 files changed, 225 insertions(+), 124 deletions(-) diff --git a/cmd/soundtouch-cli/common.go b/cmd/soundtouch-cli/common.go index d104797..ab2db35 100644 --- a/cmd/soundtouch-cli/common.go +++ b/cmd/soundtouch-cli/common.go @@ -7,6 +7,7 @@ import ( "io" "net" "net/http" + "os" "regexp" "runtime" "strconv" @@ -331,7 +332,7 @@ func PrintWarning(message string) { // showVersionInfo displays detailed version information including build details func showVersionInfo(_ *cli.Context) error { - fmt.Printf("soundtouch-cli version %s\n", version) + fmt.Printf("%s version %s\n", os.Args[0], version) fmt.Printf("Build commit: %s\n", commit) fmt.Printf("Build date: %s\n", date) fmt.Printf("Go version: %s\n", runtime.Version()) diff --git a/cmd/soundtouch-cli/main.go b/cmd/soundtouch-cli/main.go index 8cf25ad..4db02ce 100644 --- a/cmd/soundtouch-cli/main.go +++ b/cmd/soundtouch-cli/main.go @@ -104,7 +104,7 @@ func main() { Version: version, Authors: []*cli.Author{ { - Name: "Tobias Gesellchen, and the SoundTouch CLI Contributors", + Name: "Tobias Gesellchen, and the Bose-SoundTouch Contributors", }, }, Flags: CommonFlags, diff --git a/cmd/soundtouch-service/main.go b/cmd/soundtouch-service/main.go index c2705ec..d269376 100644 --- a/cmd/soundtouch-service/main.go +++ b/cmd/soundtouch-service/main.go @@ -5,7 +5,6 @@ package main import ( "context" "crypto/tls" - "flag" "fmt" "log" "net/http" @@ -13,6 +12,8 @@ import ( "net/url" "os" "path/filepath" + "runtime" + "runtime/debug" "strings" "time" @@ -23,85 +24,206 @@ import ( "github.com/gesellix/bose-soundtouch/pkg/service/setup" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" + "github.com/urfave/cli/v2" ) +var ( + version = "dev" + commit = "unknown" + date = "unknown" +) + +func updateBuildInfo() { + if info, ok := debug.ReadBuildInfo(); ok { + if info.Main.Version != "" && info.Main.Version != "(devel)" { + version = info.Main.Version + } + + for _, setting := range info.Settings { + switch setting.Key { + case "vcs.revision": + commit = setting.Value + case "vcs.time": + if t, err := time.Parse(time.RFC3339, setting.Value); err == nil { + date = t.Format("2006-01-02_15:04:05") + } + } + } + } +} + func main() { - config := loadConfig() - ds := initDataStore(config.dataDir) - cm := initCertificateManager(config.dataDir) - sm := setup.NewManager(config.serverURL, ds, cm) - server := handlers.NewServer(ds, sm, config.serverURL, config.redact, config.logBody, config.record) + updateBuildInfo() - recorder := proxy.NewRecorder(config.dataDir) - recorder.Redact = config.redact - patternsPath := filepath.Join(config.dataDir, "patterns.json") + app := &cli.App{ + Name: "soundtouch-service", + Usage: "Local service for Bose SoundTouch cloud emulation and management", + Description: `⠎⠕⠥⠝⠙⠤⠞⠕⠥⠉⠓ A local server that emulates Bose cloud services (BMX, Marge). + It enables offline operation, device migration, and HTTP interaction recording.`, + Version: version, + Authors: []*cli.Author{ + { + Name: "Tobias Gesellchen, and the Bose-SoundTouch Contributors", + }, + }, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "port", + Aliases: []string{"p"}, + Usage: "HTTP port to bind the service to", + Value: "8000", + EnvVars: []string{"PORT"}, + }, + &cli.StringFlag{ + Name: "bind", + Usage: "Network interface to bind to", + EnvVars: []string{"BIND_ADDR"}, + }, + &cli.StringFlag{ + Name: "target-url", + Usage: "URL for Python-based service components (legacy)", + Value: "http://localhost:8001", + EnvVars: []string{"PYTHON_BACKEND_URL", "TARGET_URL"}, + }, + &cli.StringFlag{ + Name: "data-dir", + Usage: "Directory for persistent data", + Value: "data", + EnvVars: []string{"DATA_DIR"}, + }, + &cli.StringFlag{ + Name: "server-url", + Aliases: []string{"s"}, + Usage: "External URL of this service", + EnvVars: []string{"SERVER_URL"}, + }, + &cli.StringFlag{ + Name: "https-port", + Usage: "HTTPS port to bind the service to", + Value: "8443", + EnvVars: []string{"HTTPS_PORT"}, + }, + &cli.StringFlag{ + Name: "https-server-url", + Aliases: []string{"S"}, + Usage: "External HTTPS URL", + EnvVars: []string{"HTTPS_SERVER_URL"}, + }, + &cli.BoolFlag{ + Name: "redact-logs", + Usage: "Redact sensitive data in proxy logs", + Value: true, + EnvVars: []string{"REDACT_PROXY_LOGS"}, + }, + &cli.BoolFlag{ + Name: "log-bodies", + Usage: "Log full request/response bodies", + EnvVars: []string{"LOG_PROXY_BODY"}, + }, + &cli.BoolFlag{ + Name: "record-interactions", + Usage: "Record HTTP interactions to disk", + Value: true, + EnvVars: []string{"RECORD_INTERACTIONS"}, + }, + &cli.StringFlag{ + Name: "discovery-interval", + Usage: "Device discovery interval", + Value: "5m", + EnvVars: []string{"DISCOVERY_INTERVAL"}, + }, + }, + Action: func(c *cli.Context) error { + config := loadConfig(c) + ds := initDataStore(config.dataDir) + cm := initCertificateManager(config.dataDir) + sm := setup.NewManager(config.serverURL, ds, cm) + server := handlers.NewServer(ds, sm, config.serverURL, config.redact, config.logBody, config.record) - patterns, err := proxy.LoadPatterns(patternsPath) - if err == nil && len(patterns) > 0 { - recorder.Patterns = patterns - } else if err != nil { - log.Printf("Warning: Failed to load patterns from %s: %v", patternsPath, err) + recorder := proxy.NewRecorder(config.dataDir) + recorder.Redact = config.redact + patternsPath := filepath.Join(config.dataDir, "patterns.json") + + patterns, err := proxy.LoadPatterns(patternsPath) + if err == nil && len(patterns) > 0 { + recorder.Patterns = patterns + } else if err != nil { + log.Printf("Warning: Failed to load patterns from %s: %v", patternsPath, err) + } + + server.SetRecorder(recorder) + + tlsConfig, err := cm.GetServerTLSConfig(config.domains) + if err != nil { + log.Printf("Warning: Failed to setup TLS: %v", err) + } + + pyProxy := setupPythonProxy(config.targetURL, config.redact, config.logBody, recorder, server) + + startDeviceDiscovery(server, config.discoveryInterval) + + r := setupRouter(server, pyProxy) + + log.Printf("Go service starting on %s, proxying to %s", config.serverURL, config.targetURL) + + if tlsConfig != nil { + startHTTPSServer(config.httpsAddr, r, tlsConfig, config.httpsServerURL) + } + + return http.ListenAndServe(config.addr, r) + }, + Commands: []*cli.Command{ + { + Name: "version", + Aliases: []string{"v"}, + Usage: "Show detailed version information", + Action: showVersionInfo, + }, + }, } - server.SetRecorder(recorder) - - tlsConfig, err := cm.GetServerTLSConfig(config.domains) - if err != nil { - log.Printf("Warning: Failed to setup TLS: %v", err) + if err := app.Run(os.Args); err != nil { + log.Fatal(err) } +} - pyProxy := setupPythonProxy(config.targetURL, config.redact, config.logBody, recorder, server) +func showVersionInfo(_ *cli.Context) error { + fmt.Printf("%s version %s\n", os.Args[0], version) + fmt.Printf("Build commit: %s\n", commit) + fmt.Printf("Build date: %s\n", date) + fmt.Printf("Go version: %s\n", runtime.Version()) + fmt.Printf("Platform: %s/%s\n", runtime.GOOS, runtime.GOARCH) - startDeviceDiscovery(server) - - r := setupRouter(server, pyProxy) - - log.Printf("Go service starting on %s, proxying to %s", config.serverURL, config.targetURL) - - if tlsConfig != nil { - startHTTPSServer(config.httpsAddr, r, tlsConfig, config.httpsServerURL) - } - - log.Fatal(http.ListenAndServe(config.addr, r)) + return nil } type serviceConfig struct { - port string - bindAddr string - addr string - targetURL string - dataDir string - serverURL string - httpsServerURL string - httpsAddr string - redact bool - logBody bool - record bool - domains []string + port string + bindAddr string + addr string + targetURL string + dataDir string + serverURL string + httpsServerURL string + httpsAddr string + redact bool + logBody bool + record bool + discoveryInterval time.Duration + domains []string } -func loadConfig() serviceConfig { - // Define flags - fPort, fBindAddr, fTargetURL, fDataDir, fServerURL, fHTTPSPort, fHTTPSServerURL, fRedact, fLogBody, fRecord := defineFlags() - - flag.Usage = func() { - fmt.Fprintf(os.Stderr, "Usage of soundtouch-service:\n") - flag.PrintDefaults() - fmt.Fprintf(os.Stderr, "\nConfiguration can also be set via environment variables.\n") - } - - flag.Parse() - - port := getEnvOrDefault("PORT", *fPort, "8000") - bindAddr := getEnvOrDefault("BIND_ADDR", *fBindAddr, "") +func loadConfig(c *cli.Context) serviceConfig { + port := c.String("port") + bindAddr := c.String("bind") addr := bindAddr + ":" + port if bindAddr == "" { addr = ":" + port } - targetURL := getEnvOrDefault("PYTHON_BACKEND_URL", *fTargetURL, "http://localhost:8001") - dataDir := getEnvOrDefault("DATA_DIR", *fDataDir, "data") + targetURL := c.String("target-url") + dataDir := c.String("data-dir") hostname, _ := os.Hostname() if hostname == "" { @@ -110,78 +232,55 @@ func loadConfig() serviceConfig { hostname = strings.ToLower(hostname) - serverURL := getEnvOrDefault("SERVER_URL", *fServerURL, "") + serverURL := c.String("server-url") if serverURL == "" { serverURL = "http://" + hostname + ":" + port } - httpsPort := getEnvOrDefault("HTTPS_PORT", *fHTTPSPort, "8443") + httpsPort := c.String("https-port") httpsAddr := bindAddr + ":" + httpsPort if bindAddr == "" { httpsAddr = ":" + httpsPort } - httpsServerURL := getEnvOrDefault("HTTPS_SERVER_URL", *fHTTPSServerURL, "") + httpsServerURL := c.String("https-server-url") if httpsServerURL == "" { httpsServerURL = "https://" + hostname + ":" + httpsPort } domains := getDomains(serverURL, httpsServerURL, hostname) - redactVal := getEnvOrDefault("REDACT_PROXY_LOGS", *fRedact, "") - redact := redactVal != "false" + redact := c.Bool("redact-logs") + logBody := c.Bool("log-bodies") + record := c.Bool("record-interactions") - logBodyVal := getEnvOrDefault("LOG_PROXY_BODY", *fLogBody, "") - logBody := logBodyVal == "true" + discoveryIntervalStr := c.String("discovery-interval") - recordVal := getEnvOrDefault("RECORD_INTERACTIONS", *fRecord, "") - record := recordVal != "false" + discoveryInterval, err := time.ParseDuration(discoveryIntervalStr) + if err != nil { + log.Printf("Warning: Failed to parse discovery interval %s, using default 5m: %v", discoveryIntervalStr, err) + + discoveryInterval = 5 * time.Minute + } return serviceConfig{ - port: port, - bindAddr: bindAddr, - addr: addr, - targetURL: targetURL, - dataDir: dataDir, - serverURL: serverURL, - httpsServerURL: httpsServerURL, - httpsAddr: httpsAddr, - redact: redact, - logBody: logBody, - record: record, - domains: domains, + port: port, + bindAddr: bindAddr, + addr: addr, + targetURL: targetURL, + dataDir: dataDir, + serverURL: serverURL, + httpsServerURL: httpsServerURL, + httpsAddr: httpsAddr, + redact: redact, + logBody: logBody, + record: record, + discoveryInterval: discoveryInterval, + domains: domains, } } -func defineFlags() (port, bind, target, data, server, httpsPort, httpsServer, redact, logBody, record *string) { - port = flag.String("port", "", "Port to bind the service to (env: PORT)") - bind = flag.String("bind", "", "Network interface to bind to (env: BIND_ADDR)") - target = flag.String("target-url", "", "URL for Python-based service components (env: PYTHON_BACKEND_URL)") - data = flag.String("data-dir", "", "Directory for persistent data (env: DATA_DIR)") - server = flag.String("server-url", "", "External URL of this service (env: SERVER_URL)") - httpsPort = flag.String("https-port", "", "HTTPS port to bind the service to (env: HTTPS_PORT)") - httpsServer = flag.String("https-server-url", "", "External HTTPS URL (env: HTTPS_SERVER_URL)") - redact = flag.String("redact-logs", "", "Redact sensitive data in proxy logs (true/false, env: REDACT_PROXY_LOGS)") - logBody = flag.String("log-bodies", "", "Log full request/response bodies (true/false, env: LOG_PROXY_BODY)") - record = flag.String("record-interactions", "", "Record HTTP interactions to disk (true/false, env: RECORD_INTERACTIONS)") - - return -} - -func getEnvOrDefault(envKey, flagVal, defaultVal string) string { - if flagVal != "" { - return flagVal - } - - val := os.Getenv(envKey) - if val != "" { - return val - } - - return defaultVal -} - func getDomains(serverURL, httpsServerURL, hostname string) []string { domainsMap := map[string]bool{ "streaming.bose.com": true, @@ -265,11 +364,11 @@ func setupPythonProxy(targetURL string, redact, logBody bool, recorder *proxy.Re return pyProxy } -func startDeviceDiscovery(server *handlers.Server) { +func startDeviceDiscovery(server *handlers.Server, interval time.Duration) { go func() { for { server.DiscoverDevices(context.Background()) - time.Sleep(5 * time.Minute) + time.Sleep(interval) } }() } diff --git a/docs/SOUNDTOUCH-SERVICE.md b/docs/SOUNDTOUCH-SERVICE.md index 10a1515..685df1f 100644 --- a/docs/SOUNDTOUCH-SERVICE.md +++ b/docs/SOUNDTOUCH-SERVICE.md @@ -135,18 +135,19 @@ Use the web interface or API to migrate devices from Bose cloud services to your The service can be configured via environment variables or command-line flags: -| Variable | Flag | Description | Default | -|-----------------------|-------------------------|--------------------------------------------------|---------------------------| -| `PORT` | `--port` | Port to bind the service to | `8000` | -| `BIND_ADDR` | `--bind` | Network interface to bind to | all (ipv4 and ipv6) | -| `DATA_DIR` | `--data-dir` | Directory for persistent data | `./data` | -| `SERVER_URL` | `--server-url` | External URL of this service | `http://:8000` | -| `HTTPS_SERVER_URL` | `--https-server-url` | External HTTPS URL | `https://:8443` | -| `PYTHON_BACKEND_URL` | | URL for Python-based service components (legacy) | | -| `REDACT_PROXY_LOGS` | `--redact-logs` | Redact sensitive data in proxy logs | `true` | -| `LOG_PROXY_BODY` | `--log-bodies` | Log full request/response bodies | `false` | -| `RECORD_INTERACTIONS` | `--record-interactions` | Record HTTP interactions to disk | `true` | -| `DISCOVERY_INTERVAL` | `--discovery-interval` | Device discovery interval | `5m` | +| Variable | Flag | Description | Default | +|------------------------------------|----------------------------|--------------------------------------------------|---------------------------| +| `PORT` | `--port`, `-p` | HTTP port to bind the service to | `8000` | +| `BIND_ADDR` | `--bind` | Network interface to bind to | all (ipv4 and ipv6) | +| `DATA_DIR` | `--data-dir` | Directory for persistent data | `./data` | +| `SERVER_URL` | `--server-url`, `-s` | External URL of this service | `http://:8000` | +| `HTTPS_PORT` | `--https-port` | HTTPS port to bind the service to | `8443` | +| `HTTPS_SERVER_URL` | `--https-server-url`, `-S` | External HTTPS URL | `https://:8443` | +| `PYTHON_BACKEND_URL`, `TARGET_URL` | `--target-url` | URL for Python-based service components (legacy) | `http://localhost:8001` | +| `REDACT_PROXY_LOGS` | `--redact-logs` | Redact sensitive data in proxy logs | `true` | +| `LOG_PROXY_BODY` | `--log-bodies` | Log full request/response bodies | `false` | +| `RECORD_INTERACTIONS` | `--record-interactions` | Record HTTP interactions to disk | `true` | +| `DISCOVERY_INTERVAL` | `--discovery-interval` | Device discovery interval | `5m` | ### Configuration Examples