From bd62fd6658d874e66d828315e9791d58686a156a Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Sun, 7 Jun 2026 16:00:28 +0200 Subject: [PATCH] refactor: rename soundtouch-web to soundtouch-player (transitional alias) (refs #451) The web player is intrinsically LAN-resident: it reaches speakers directly and only delegates cloud-only features (e.g. TTS) to a possibly-remote AfterTouch service via --service-url. That is exactly what a cloud-hosted soundtouch-service cannot do, so the standalone player binary stays useful and is not being deprecated. Rename it to state its purpose, with a transition window so existing downloads keep working. - cmd/soundtouch-web -> cmd/soundtouch-player; CLI name is now soundtouch-player. When the binary is invoked under its old name it prints a one-line rename notice (filepath.Base(os.Args[0])). - Build/release both names from the same source: Makefile (build-player + build-web alias, dev-player* targets), Dockerfile (soundtouch-player image + transitional soundtouch-web image), release.yml and ci.yml (player + web artifacts, checksums, Docker images; release notes announce the rename). The soundtouch-web binary, image, and install script remain a transitional alias to be dropped in a future release (which will break stale fetch scripts and nudge users to the release notes). - scripts/raspberry-pi/install-player.sh is canonical; install-web.sh keeps working but warns. - Sweep docs, code comments, user-facing strings, and assets (soundtouch-web-ui.png, soundtouch-web-tunein.png, soundtouch-web-roadmap.md) to soundtouch-player; README documents the rename and why the player remains separate from the embedded /app. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 24 +- .github/workflows/release.yml | 60 +++- .gitignore | 1 + .golangci.yml | 2 +- CLAUDE.md | 4 +- Dockerfile | 27 +- Makefile | 57 +-- README.md | 8 +- cmd/soundtouch-player/.gitignore | 4 + .../IMPLEMENTATION.md | 22 +- .../README.md | 24 +- .../logutil.go | 0 .../main.go | 32 +- .../resolve_bind_addr_test.go | 0 .../spa_test.go | 0 cmd/soundtouch-service/main.go | 4 +- cmd/soundtouch-service/router_test.go | 2 +- cmd/soundtouch-web/.gitignore | 2 - docs/archive/PLAN.md | 6 +- docs/content/_index.md | 2 +- docs/content/blog/2026-05-welcome.md | 12 +- .../bose-soundtouch-community-tools.md | 10 +- .../docs/appendix/PRESET-QUICKSTART.md | 6 +- ...oadmap.md => soundtouch-player-roadmap.md} | 10 +- .../docs/architecture/API-ROUTE-LAYOUT.md | 24 +- .../docs/architecture/DEVICE-LOCAL-INSTALL.md | 12 +- docs/content/docs/architecture/_index.md | 2 +- .../docs/guides/EXTERNAL-HOST-WALKTHROUGH.md | 16 +- docs/content/docs/guides/RASPBERRY-PI.md | 36 +- .../content/docs/guides/SOUNDTOUCH-SERVICE.md | 2 +- docs/content/docs/guides/SURVIVAL-GUIDE.md | 4 +- docs/content/docs/guides/TROUBLESHOOTING.md | 16 +- .../docs/reference/SPEAKER-ENDPOINT.md | 2 +- ...ch-web-ui.png => soundtouch-player-ui.png} | Bin ...unein.png => soundtouch-player-tunein.png} | Bin pkg/service/handlers/handlers_tts_test.go | 2 +- pkg/service/soundtouchweb/discovery.go | 4 +- pkg/service/soundtouchweb/handler.go | 10 +- pkg/service/soundtouchweb/handlers_tts.go | 6 +- pkg/service/soundtouchweb/logutil.go | 2 +- pkg/service/soundtouchweb/mount.go | 4 +- pkg/service/soundtouchweb/service_client.go | 2 +- .../static/js/components/PlayURL.js | 2 +- .../soundtouchweb/static/js/components/TTS.js | 10 +- scripts/raspberry-pi/README.md | 4 +- scripts/raspberry-pi/install-player.sh | 338 ++++++++++++++++++ scripts/raspberry-pi/install-web.sh | 11 + 47 files changed, 639 insertions(+), 189 deletions(-) create mode 100644 cmd/soundtouch-player/.gitignore rename cmd/{soundtouch-web => soundtouch-player}/IMPLEMENTATION.md (94%) rename cmd/{soundtouch-web => soundtouch-player}/README.md (96%) rename cmd/{soundtouch-web => soundtouch-player}/logutil.go (100%) rename cmd/{soundtouch-web => soundtouch-player}/main.go (84%) rename cmd/{soundtouch-web => soundtouch-player}/resolve_bind_addr_test.go (100%) rename cmd/{soundtouch-web => soundtouch-player}/spa_test.go (100%) delete mode 100644 cmd/soundtouch-web/.gitignore rename docs/content/docs/appendix/{soundtouch-web-roadmap.md => soundtouch-player-roadmap.md} (95%) rename docs/static/images/blog/{soundtouch-web-ui.png => soundtouch-player-ui.png} (100%) rename docs/static/images/{soundtouch-web-tunein.png => soundtouch-player-tunein.png} (100%) create mode 100755 scripts/raspberry-pi/install-player.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fdfb2cd..81a6211 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -143,7 +143,9 @@ jobs: mkdir -p build - for binary in soundtouch-cli soundtouch-service soundtouch-web soundtouch-backup; do + # soundtouch-web is now a transitional alias of soundtouch-player + # (same source); building the player is enough to verify both. + for binary in soundtouch-cli soundtouch-service soundtouch-player soundtouch-backup; do OUTPUT="build/${binary}-${ARCH_SUFFIX}${EXT}" echo "Building $OUTPUT" go build -trimpath -ldflags="-s -w" -o "$OUTPUT" "./cmd/$binary" @@ -360,26 +362,26 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max - - name: Extract metadata (tags, labels) for soundtouch-web - id: meta-web + - name: Extract metadata (tags, labels) for soundtouch-player + id: meta-player uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 with: - images: ghcr.io/${{ github.repository }}-web + images: ghcr.io/${{ github.repository }}-player tags: | type=raw,value=edge,enable=${{ github.ref == 'refs/heads/main' }} type=ref,event=pr,prefix=preview-pr- type=sha,prefix=preview-sha-,format=short,enable=${{ github.event_name == 'pull_request' }} type=ref,event=branch,prefix=preview-branch-,enable=${{ github.event_name == 'push' && github.ref != 'refs/heads/main' }} - - name: Build and push soundtouch-web Docker image + - name: Build and push soundtouch-player Docker image uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . - target: soundtouch-web + target: soundtouch-player platforms: linux/amd64,linux/arm64,linux/arm64/v8,linux/arm/v7 push: ${{ steps.push-check.outputs.should-push == 'true' }} - tags: ${{ steps.meta-web.outputs.tags }} - labels: ${{ steps.meta-web.outputs.labels }} + tags: ${{ steps.meta-player.outputs.tags }} + labels: ${{ steps.meta-player.outputs.labels }} build-args: | COMMIT=${{ github.sha }} DATE=${{ steps.build_date.outputs.date }} @@ -390,7 +392,7 @@ jobs: if: steps.push-check.outputs.should-push == 'true' env: SERVICE_TAGS: ${{ steps.meta-service.outputs.tags }} - WEB_TAGS: ${{ steps.meta-web.outputs.tags }} + PLAYER_TAGS: ${{ steps.meta-player.outputs.tags }} EVENT_NAME: ${{ github.event_name }} PR_NUMBER: ${{ github.event.pull_request.number }} REF_NAME: ${{ github.ref_name }} @@ -414,12 +416,12 @@ jobs: done <<< "$SERVICE_TAGS" echo '```' echo "" - echo "### soundtouch-web" + echo "### soundtouch-player" echo "" echo '```bash' while IFS= read -r tag; do [[ -n "$tag" ]] && echo "docker pull $tag" - done <<< "$WEB_TAGS" + done <<< "$PLAYER_TAGS" echo '```' } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cc66d03..cab89f6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -170,8 +170,12 @@ jobs: # Build Service build_binary "soundtouch-service" "./cmd/soundtouch-service" - # Build Web - build_binary "soundtouch-web" "./cmd/soundtouch-web" + # Build Player (formerly soundtouch-web) + build_binary "soundtouch-player" "./cmd/soundtouch-player" + + # Build Web: transitional alias of the player, built from the same + # source. Dropped in a future release; keep in sync with player. + build_binary "soundtouch-web" "./cmd/soundtouch-player" # Build Backup build_binary "soundtouch-backup" "./cmd/soundtouch-backup" @@ -181,6 +185,7 @@ jobs: run: | CLI_NAME="${{ steps.build.outputs.soundtouch-cli }}" SVC_NAME="${{ steps.build.outputs.soundtouch-service }}" + PLAYER_NAME="${{ steps.build.outputs.soundtouch-player }}" WEB_NAME="${{ steps.build.outputs.soundtouch-web }}" BCK_NAME="${{ steps.build.outputs.soundtouch-backup }}" @@ -198,6 +203,7 @@ jobs: generate_checksums "$CLI_NAME" generate_checksums "$SVC_NAME" + generate_checksums "$PLAYER_NAME" generate_checksums "$WEB_NAME" generate_checksums "$BCK_NAME" @@ -212,6 +218,7 @@ jobs: path: | build/soundtouch-cli-v* build/soundtouch-service-v* + build/soundtouch-player-v* build/soundtouch-web-v* build/soundtouch-backup-v* retention-days: 1 @@ -240,7 +247,7 @@ jobs: mkdir -p release-files # Move all files from subdirectories to the collection directory - find . -mindepth 2 -type f \( -name "soundtouch-cli-*" -o -name "soundtouch-service-*" -o -name "soundtouch-web-*" -o -name "soundtouch-backup-*" \) -exec mv {} release-files/ \; + find . -mindepth 2 -type f \( -name "soundtouch-cli-*" -o -name "soundtouch-service-*" -o -name "soundtouch-player-*" -o -name "soundtouch-web-*" -o -name "soundtouch-backup-*" \) -exec mv {} release-files/ \; # Remove empty directories find . -type d -empty -delete @@ -255,14 +262,14 @@ jobs: # Generate combined checksums (exclude individual .sha256/.sha512 files) if ls soundtouch-* 1> /dev/null 2>&1; then # Only checksum the actual binaries, not the .sha256/.sha512 files - ls soundtouch-cli-* soundtouch-service-* soundtouch-web-* soundtouch-backup-* | grep -v '\.sha256$' | grep -v '\.sha512$' | xargs sha256sum > checksums.sha256 - ls soundtouch-cli-* soundtouch-service-* soundtouch-web-* soundtouch-backup-* | grep -v '\.sha256$' | grep -v '\.sha512$' | xargs sha512sum > checksums.sha512 + ls soundtouch-cli-* soundtouch-service-* soundtouch-player-* soundtouch-web-* soundtouch-backup-* | grep -v '\.sha256$' | grep -v '\.sha512$' | xargs sha256sum > checksums.sha256 + ls soundtouch-cli-* soundtouch-service-* soundtouch-player-* soundtouch-web-* soundtouch-backup-* | grep -v '\.sha256$' | grep -v '\.sha512$' | xargs sha512sum > checksums.sha512 echo "πŸ“‹ Generated combined checksums:" cat checksums.sha256 # Verify all expected files are present (binaries only, not checksum files) - EXPECTED_COUNT=28 # 7 platforms * 4 binaries + EXPECTED_COUNT=35 # 7 platforms * 5 binaries (player + its web alias) ACTUAL_COUNT=$(ls soundtouch-* | grep -v '\.sha256$' | grep -v '\.sha512$' | wc -l) if [[ $ACTUAL_COUNT -ne $EXPECTED_COUNT ]]; then @@ -393,11 +400,15 @@ jobs: ./soundtouch-service \`\`\` - ### SoundTouch Web + ### SoundTouch Player (formerly soundtouch-web) \`\`\`bash - # Start the web app - ./soundtouch-web + # Start the LAN web player + ./soundtouch-player \`\`\` + > Note: \`soundtouch-web\` has been renamed to \`soundtouch-player\`. + > The \`soundtouch-web\` assets are still published as a transitional + > alias and will be removed in a future release. Please switch your + > downloads and scripts to \`soundtouch-player\`. ### SoundTouch Backup \`\`\`bash @@ -423,7 +434,7 @@ jobs: - Windows (amd64) - FreeBSD (amd64) - `soundtouch-cli`, `soundtouch-service`, `soundtouch-web`, and `soundtouch-backup` are included. + `soundtouch-cli`, `soundtouch-service`, `soundtouch-player` (with `soundtouch-web` as a transitional alias), and `soundtouch-backup` are included. ## πŸ” Checksums @@ -478,6 +489,7 @@ jobs: files: | release-assets/soundtouch-cli-v* release-assets/soundtouch-service-v* + release-assets/soundtouch-player-v* release-assets/soundtouch-web-v* release-assets/soundtouch-backup-v* release-assets/checksums.sha256 @@ -506,6 +518,7 @@ jobs: files: | release-assets/soundtouch-cli-v* release-assets/soundtouch-service-v* + release-assets/soundtouch-player-v* release-assets/soundtouch-web-v* release-assets/soundtouch-backup-v* release-assets/checksums.sha256 @@ -563,6 +576,33 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max + - name: Extract metadata (tags, labels) for soundtouch-player + id: meta-player + uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 + with: + images: ghcr.io/${{ github.repository }}-player + tags: | + type=semver,pattern={{version}},value=v${{ needs.validate.outputs.version }} + type=semver,pattern={{major}}.{{minor}},value=v${{ needs.validate.outputs.version }} + type=raw,value=latest,enable=${{ needs.validate.outputs.is_prerelease == 'false' }} + + - name: Build and push soundtouch-player Docker image + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + with: + context: . + target: soundtouch-player + platforms: linux/amd64,linux/arm64,linux/arm64/v8,linux/arm/v7 + push: true + tags: ${{ steps.meta-player.outputs.tags }} + labels: ${{ steps.meta-player.outputs.labels }} + build-args: | + VERSION=v${{ needs.validate.outputs.version }} + COMMIT=${{ github.sha }} + DATE=${{ steps.build_date.outputs.date }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # Transitional alias image (formerly the only web image). Dropped later. - name: Extract metadata (tags, labels) for soundtouch-web id: meta-web uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 diff --git a/.gitignore b/.gitignore index 70b228c..720bcad 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ dist/ /soundtouch-backup /soundtouch-cli /soundtouch-service +/soundtouch-player /soundtouch-web /dummy-speaker /example-mdns diff --git a/.golangci.yml b/.golangci.yml index df4e359..056173f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -78,7 +78,7 @@ linters: linters: - errcheck - # Carry-over from cmd/soundtouch-web/handlers relocation: same code, + # Carry-over from cmd/soundtouch-player/handlers relocation: same code, # same waiver. Tighten in a follow-up if/when the package is reviewed. - path: pkg/service/soundtouchweb/.*\.go text: "Error return value of.*is not checked" diff --git a/CLAUDE.md b/CLAUDE.md index 4ff9a06..7839cf7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,7 +18,7 @@ Key binaries: (status, play, presets, groups, migration, …). - `soundtouch-service` β€” replacement for `streaming.bose.com` and the `bmx` services, default port `8000`. -- `soundtouch-web` β€” Web UI for Radio browsing and device control. +- `soundtouch-player` β€” Web UI for Radio browsing and device control. - `soundtouch-backup` β€” Helper for on-device backup and restore. Per-session pickup notes live in two local files at the repo root (they are `.gitignore`d and only exist if created during a session): @@ -105,7 +105,7 @@ retrospective diffing whenever something goes sideways. cmd/ soundtouch-cli/ # CLI tool for device control soundtouch-service/ # Local cloud service emulator - soundtouch-web/ # Web UI (TuneIn browser, device control) + soundtouch-player/ # Web UI (TuneIn browser, device control) soundtouch-backup/ # On-device backup helper example-*/ # Usage examples pkg/ diff --git a/Dockerfile b/Dockerfile index 645654e..7ce6356 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,15 +34,15 @@ RUN if [ "${TARGETARCH}" = "arm" ] && [ -n "${TARGETVARIANT}" ]; then \ -o /soundtouch-service ./cmd/soundtouch-service; \ fi -# Build the soundtouch-web +# Build the soundtouch-player (formerly soundtouch-web) RUN if [ "${TARGETARCH}" = "arm" ] && [ -n "${TARGETVARIANT}" ]; then \ CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOARM=${TARGETVARIANT#v} \ go build -trimpath -ldflags="-s -w -X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=${DATE}" \ - -o /soundtouch-web ./cmd/soundtouch-web; \ + -o /soundtouch-player ./cmd/soundtouch-player; \ else \ CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ go build -trimpath -ldflags="-s -w -X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=${DATE}" \ - -o /soundtouch-web ./cmd/soundtouch-web; \ + -o /soundtouch-player ./cmd/soundtouch-player; \ fi # soundtouch-service image @@ -68,14 +68,31 @@ EXPOSE 8000 ENTRYPOINT ["/app/soundtouch-service"] -# soundtouch-web image +# soundtouch-player image +FROM alpine:3.23 AS soundtouch-player + +RUN apk add --no-cache ca-certificates tzdata + +WORKDIR /app + +COPY --from=builder /soundtouch-player /app/soundtouch-player + +ENV PORT=8080 + +EXPOSE 8080 + +ENTRYPOINT ["/app/soundtouch-player"] + +# soundtouch-web image: transitional alias of soundtouch-player. Built from the +# same binary; the entrypoint name makes the binary print a rename notice on +# start. Will be dropped in a future release. FROM alpine:3.23 AS soundtouch-web RUN apk add --no-cache ca-certificates tzdata WORKDIR /app -COPY --from=builder /soundtouch-web /app/soundtouch-web +COPY --from=builder /soundtouch-player /app/soundtouch-web ENV PORT=8080 diff --git a/Makefile b/Makefile index a97a54e..f48f827 100644 --- a/Makefile +++ b/Makefile @@ -17,8 +17,11 @@ BINARY_NAME=soundtouch-cli BINARY_PATH=./cmd/$(BINARY_NAME) SERVICE_NAME=soundtouch-service SERVICE_PATH=./cmd/$(SERVICE_NAME) +PLAYER_NAME=soundtouch-player +PLAYER_PATH=./cmd/$(PLAYER_NAME) +# WEB_NAME is the previous name for the player, kept as a transitional alias +# built from the same PLAYER_PATH source. It will be dropped in a future release. WEB_NAME=soundtouch-web -WEB_PATH=./cmd/$(WEB_NAME) EXAMPLE_MDNS_NAME=example-mdns EXAMPLE_MDNS_PATH=./cmd/$(EXAMPLE_MDNS_NAME) EXAMPLE_UPNP_NAME=example-upnp @@ -52,7 +55,7 @@ AUTH_SERVICE_URL ?= $(BACKEND_URL) all: check build -build: build-cli build-service build-web build-examples build-favicon-gen build-backup +build: build-cli build-service build-player build-web build-examples build-favicon-gen build-backup build-cli: @echo "Building $(BINARY_NAME)..." @@ -64,10 +67,17 @@ build-service: @mkdir -p $(BUILD_DIR) $(GOBUILD) $(BUILDFLAGS) -o $(BUILD_DIR)/$(SERVICE_NAME) $(SERVICE_PATH) -build-web: - @echo "Building $(WEB_NAME)..." +build-player: + @echo "Building $(PLAYER_NAME)..." @mkdir -p $(BUILD_DIR) - $(GOBUILD) $(BUILDFLAGS) -o $(BUILD_DIR)/$(WEB_NAME) $(WEB_PATH) + $(GOBUILD) $(BUILDFLAGS) -o $(BUILD_DIR)/$(PLAYER_NAME) $(PLAYER_PATH) + +# Transitional alias: builds the same source as build-player under the old +# soundtouch-web name. Drop this target once the alias is retired. +build-web: + @echo "Building $(WEB_NAME) (transitional alias of $(PLAYER_NAME))..." + @mkdir -p $(BUILD_DIR) + $(GOBUILD) $(BUILDFLAGS) -o $(BUILD_DIR)/$(WEB_NAME) $(PLAYER_PATH) build-examples: @echo "Building $(EXAMPLE_MDNS_NAME)..." @@ -334,17 +344,17 @@ dev-scan-http: build-examples @echo "Scanning for HTTP mDNS services..." $(BUILD_DIR)/$(SCANNER_NAME) -service _http._tcp -v -dev-web: build-web - @echo "Starting web UI (default port 8080)..." - cd cmd/soundtouch-web && ../../$(BUILD_DIR)/$(WEB_NAME) +dev-player: build-player + @echo "Starting web player (default port 8080)..." + cd cmd/soundtouch-player && ../../$(BUILD_DIR)/$(PLAYER_NAME) -dev-web-port: build-web - @echo "Starting web UI on custom port..." +dev-player-port: build-player + @echo "Starting web player on custom port..." @if [ -z "$(PORT)" ]; then \ - echo "Usage: make dev-web-port PORT=8888"; \ + echo "Usage: make dev-player-port PORT=8888"; \ exit 1; \ fi - cd cmd/soundtouch-web && ../../$(BUILD_DIR)/$(WEB_NAME) -port $(PORT) + cd cmd/soundtouch-player && ../../$(BUILD_DIR)/$(PLAYER_NAME) -port $(PORT) dev-backup: build-backup @echo "Running backup tool..." @@ -358,18 +368,19 @@ dev-backup-local: build-backup @echo "Running local backup (auto-discover)..." $(BUILD_DIR)/$(BACKUP_NAME) local --discover -dev-web-host: build-web - @echo "Starting web UI with specific host..." +dev-player-host: build-player + @echo "Starting web player with specific host..." @if [ -z "$(HOST)" ]; then \ - echo "Usage: make dev-web-host HOST=192.0.2.10"; \ + echo "Usage: make dev-player-host HOST=192.0.2.10"; \ exit 1; \ fi - cd cmd/soundtouch-web && ../../$(BUILD_DIR)/$(WEB_NAME) -host $(HOST) + cd cmd/soundtouch-player && ../../$(BUILD_DIR)/$(PLAYER_NAME) -host $(HOST) -install: build-cli build-service build-web build-backup +install: build-cli build-service build-player build-web build-backup @echo "Installing binaries to $(GOPATH)/bin..." cp $(BUILD_DIR)/$(BINARY_NAME) $(GOPATH)/bin/ cp $(BUILD_DIR)/$(SERVICE_NAME) $(GOPATH)/bin/ + cp $(BUILD_DIR)/$(PLAYER_NAME) $(GOPATH)/bin/ cp $(BUILD_DIR)/$(WEB_NAME) $(GOPATH)/bin/ cp $(BUILD_DIR)/$(BACKUP_NAME) $(GOPATH)/bin/ @@ -530,9 +541,9 @@ help: @echo " dev-backup - Build and show backup tool help" @echo " dev-backup-cloud - Build and run cloud backup (prompts for credentials)" @echo " dev-backup-local - Build and run local backup (auto-discover speakers)" - @echo " dev-web - Build and run web UI (default port 8080)" - @echo " dev-web-port - Build and run web UI on custom port (PORT=8888)" - @echo " dev-web-host - Build and run web UI with specific device (HOST=ip)" + @echo " dev-player - Build and run web player (default port 8080)" + @echo " dev-player-port - Build and run web player on custom port (PORT=8888)" + @echo " dev-player-host - Build and run web player with specific device (HOST=ip)" @echo " install - Install binaries to GOPATH/bin" @echo " clean - Clean build artifacts" @echo " release - Create release binaries" @@ -557,8 +568,8 @@ help: @echo " make dev-upnp-timeout TIMEOUT=10s" @echo " make dev-scan-all" @echo " make dev-scan-soundtouch" - @echo " make dev-web" - @echo " make dev-web-port PORT=8888" - @echo " make dev-web-host HOST=192.0.2.10" + @echo " make dev-player" + @echo " make dev-player-port PORT=8888" + @echo " make dev-player-host HOST=192.0.2.10" @echo " make test" @echo " make build-all" diff --git a/README.md b/README.md index f4641da..1b93f1c 100644 --- a/README.md +++ b/README.md @@ -70,11 +70,13 @@ See the [CLI Reference](https://gesellix.github.io/Bose-SoundTouch/docs/guides/C --- -### soundtouch-web +### soundtouch-player -A standalone web UI for device control β€” play, pause, volume, preset selection, real-time status β€” served from a local Go binary. Complements `soundtouch-service` when you want a dedicated device-control interface separate from the setup/admin UI. +> Formerly `soundtouch-web`. The `soundtouch-web` binary, Docker image, and install script are still published as a transitional alias and will be removed in a future release; please switch to `soundtouch-player`. -See the [soundtouch-web README](cmd/soundtouch-web/README.md) for usage. +A standalone, LAN-resident web UI for device control β€” play, pause, volume, preset selection, real-time status β€” served from a local Go binary. Because it reaches speakers directly on your network and can delegate cloud-only features (e.g. TTS) to a remote AfterTouch service via `--service-url`, it stays useful when `soundtouch-service` runs off-LAN (for example in the cloud), where the embedded `/app` player cannot reach your speakers. + +See the [soundtouch-player README](cmd/soundtouch-player/README.md) for usage. --- diff --git a/cmd/soundtouch-player/.gitignore b/cmd/soundtouch-player/.gitignore new file mode 100644 index 0000000..2610713 --- /dev/null +++ b/cmd/soundtouch-player/.gitignore @@ -0,0 +1,4 @@ +soundtouch-player +soundtouch-player-test +soundtouch-web +soundtouch-web-test diff --git a/cmd/soundtouch-web/IMPLEMENTATION.md b/cmd/soundtouch-player/IMPLEMENTATION.md similarity index 94% rename from cmd/soundtouch-web/IMPLEMENTATION.md rename to cmd/soundtouch-player/IMPLEMENTATION.md index fbdb8a1..86f6849 100644 --- a/cmd/soundtouch-web/IMPLEMENTATION.md +++ b/cmd/soundtouch-player/IMPLEMENTATION.md @@ -2,7 +2,7 @@ ## Overview -The `soundtouch-web` tool provides a modern single-page application (SPA) for controlling Bose SoundTouch devices. Built with a JSON API backend and client-side JavaScript rendering, it offers superior performance and eliminates template rendering issues. +The `soundtouch-player` tool provides a modern single-page application (SPA) for controlling Bose SoundTouch devices. Built with a JSON API backend and client-side JavaScript rendering, it offers superior performance and eliminates template rendering issues. ## Architecture @@ -141,10 +141,10 @@ GET /api/control/{id}/source?name=X # Select source ### Build Commands ```bash # Build the web application -cd cmd/soundtouch-web -go build -o soundtouch-web +cd cmd/soundtouch-player +go build -o soundtouch-player -# Build all project components (includes soundtouch-web) +# Build all project components (includes soundtouch-player) make build # Cross-platform builds @@ -154,19 +154,19 @@ make build-all ### Testing ```bash # Run unit tests -go test ./cmd/soundtouch-web/... +go test ./cmd/soundtouch-player/... # Run with coverage -go test -cover ./cmd/soundtouch-web/... +go test -cover ./cmd/soundtouch-player/... # Lint checking -golangci-lint run cmd/soundtouch-web/... +golangci-lint run cmd/soundtouch-player/... ``` ### Development Server ```bash # Run development server -cd cmd/soundtouch-web +cd cmd/soundtouch-player go run main.go -port 8080 # Access the web interface @@ -177,7 +177,7 @@ open http://localhost:8080 ### Command Line Options ```bash -soundtouch-web [options] +soundtouch-player [options] Options: -port string Web server port (default "8080") @@ -186,9 +186,9 @@ Options: ### File Structure ``` -cmd/soundtouch-web/ +cmd/soundtouch-player/ β”œβ”€β”€ main.go # Application entry point -β”œβ”€β”€ soundtouch-web # Built binary +β”œβ”€β”€ soundtouch-player # Built binary β”œβ”€β”€ handlers/ β”‚ β”œβ”€β”€ handlers.go # HTTP request handlers β”‚ β”œβ”€β”€ handlers_test.go # Handler tests diff --git a/cmd/soundtouch-web/README.md b/cmd/soundtouch-player/README.md similarity index 96% rename from cmd/soundtouch-web/README.md rename to cmd/soundtouch-player/README.md index a93279b..499e238 100644 --- a/cmd/soundtouch-web/README.md +++ b/cmd/soundtouch-player/README.md @@ -75,20 +75,20 @@ Individual device pages provide full control over: make build # Or manually -cd cmd/soundtouch-web -go build -o soundtouch-web +cd cmd/soundtouch-player +go build -o soundtouch-player ``` ### Running ```bash # Run with default settings (port 8080) -./soundtouch-web +./soundtouch-player # Specify custom port -./soundtouch-web -port 8888 +./soundtouch-player -port 8888 # Connect to specific device -./soundtouch-web -host 192.0.2.100 +./soundtouch-player -host 192.0.2.100 ``` ### Command Line Options @@ -105,17 +105,17 @@ go build -o soundtouch-web ### Text-to-Speech (TTS) TTS synthesis and the Bose `app_key` live in the AfterTouch service, not in -soundtouch-web, so the "Speak" feature proxies to the service's -`/setup/tts/speak` endpoint. To use it, point soundtouch-web at the service +soundtouch-player, so the "Speak" feature proxies to the service's +`/setup/tts/speak` endpoint. To use it, point soundtouch-player at the service with `--service-url`. When the service is served over HTTPS with its own self-signed certificate -(the default), soundtouch-web also needs to trust the service's CA, or the +(the default), soundtouch-player also needs to trust the service's CA, or the proxied call fails with `x509: certificate signed by unknown authority`. Pass the CA with `--service-ca`; it is the service's `/certs/ca.crt`: ```bash -soundtouch-web \ +soundtouch-player \ --service-url https://soundtouch.fritz.box \ --service-ca /path/to/certs/ca.crt ``` @@ -218,7 +218,7 @@ ws.onmessage = function(event) { ### Project Structure ``` -cmd/soundtouch-web/ +cmd/soundtouch-player/ β”œβ”€β”€ main.go # Application entry point and SPA routing β”œβ”€β”€ handlers/ # HTTP and WebSocket handlers β”‚ β”œβ”€β”€ handlers.go # JSON API endpoints @@ -244,7 +244,7 @@ cmd/soundtouch-web/ go test ./... # Manual testing with multiple devices -./soundtouch-web -port 8080 +./soundtouch-player -port 8080 # API testing curl http://localhost:8080/api/devices @@ -323,7 +323,7 @@ This UI is based on extensive analysis of captured SoundTouch WebSocket interact Add verbose logging by setting environment variable: ```bash export DEBUG=true -./soundtouch-web +./soundtouch-player ``` ## Contributing diff --git a/cmd/soundtouch-web/logutil.go b/cmd/soundtouch-player/logutil.go similarity index 100% rename from cmd/soundtouch-web/logutil.go rename to cmd/soundtouch-player/logutil.go diff --git a/cmd/soundtouch-web/main.go b/cmd/soundtouch-player/main.go similarity index 84% rename from cmd/soundtouch-web/main.go rename to cmd/soundtouch-player/main.go index e647865..7286bd7 100644 --- a/cmd/soundtouch-web/main.go +++ b/cmd/soundtouch-player/main.go @@ -1,4 +1,11 @@ -// Package main provides a web UI for controlling Bose SoundTouch devices. +// Package main provides soundtouch-player, the LAN-resident web player for +// controlling Bose SoundTouch devices. It reaches speakers directly on the +// local network and optionally delegates cloud-only features (e.g. TTS) to a +// remote AfterTouch service via --service-url, which is why it stays useful +// when soundtouch-service runs off-LAN (e.g. in the cloud). +// +// It was previously named soundtouch-web; that name is still published as a +// transitional alias and will be dropped in a future release. package main import ( @@ -8,6 +15,7 @@ import ( "net" "net/http" "os" + "path/filepath" "runtime/debug" "strings" "time" @@ -47,12 +55,30 @@ func updateBuildInfo() { } } +// warnIfInvokedAsWeb prints a one-line deprecation notice when the binary is +// run under its old name (soundtouch-web). The soundtouch-web artifact is a +// transitional alias built from this same source; this nudges operators to +// switch to soundtouch-player before the alias is dropped. +func warnIfInvokedAsWeb() { + if len(os.Args) == 0 { + return + } + + name := filepath.Base(os.Args[0]) + if name == "soundtouch-web" || name == "soundtouch-web.exe" { + log.Println("notice: 'soundtouch-web' has been renamed to 'soundtouch-player'. " + + "This name is a transitional alias and will stop being published in a future release; " + + "please switch to 'soundtouch-player'.") + } +} + func main() { updateBuildInfo() + warnIfInvokedAsWeb() app := &cli.App{ - Name: "soundtouch-web", - Usage: "Web UI for controlling Bose SoundTouch devices", + Name: "soundtouch-player", + Usage: "LAN web player for controlling Bose SoundTouch devices", Flags: []cli.Flag{ &cli.StringFlag{ Name: "port", diff --git a/cmd/soundtouch-web/resolve_bind_addr_test.go b/cmd/soundtouch-player/resolve_bind_addr_test.go similarity index 100% rename from cmd/soundtouch-web/resolve_bind_addr_test.go rename to cmd/soundtouch-player/resolve_bind_addr_test.go diff --git a/cmd/soundtouch-web/spa_test.go b/cmd/soundtouch-player/spa_test.go similarity index 100% rename from cmd/soundtouch-web/spa_test.go rename to cmd/soundtouch-player/spa_test.go diff --git a/cmd/soundtouch-service/main.go b/cmd/soundtouch-service/main.go index a3bfa89..5f0e4fa 100644 --- a/cmd/soundtouch-service/main.go +++ b/cmd/soundtouch-service/main.go @@ -588,7 +588,7 @@ func main() { } } - // Embedded web UI (soundtouch-web): LAN control UI under /app, control + // Embedded web UI (soundtouch-player): LAN control UI under /app, control // API under /api/control. Same LAN-trust tier as /setup, no auth. webApp := newEmbeddedWebApp(server, config.serverURL, ds) @@ -1088,7 +1088,7 @@ func startDeviceDiscovery(server *handlers.Server) { }() } -// newEmbeddedWebApp builds the soundtouch-web application for embedding in the +// newEmbeddedWebApp builds the soundtouch-player application for embedding in the // service router: release metadata from the build vars, a loopback ServiceURL // for the TTS / Play URL proxy (plain HTTP, no CA trust needed), and device // state sourced entirely from the service. diff --git a/cmd/soundtouch-service/router_test.go b/cmd/soundtouch-service/router_test.go index 9179328..1d6f82b 100644 --- a/cmd/soundtouch-service/router_test.go +++ b/cmd/soundtouch-service/router_test.go @@ -19,7 +19,7 @@ import ( func TestPrintRoutes(t *testing.T) { // Initialize a minimal server to get the router. Pass a web app so the - // snapshot also captures the embedded soundtouch-web surface + // snapshot also captures the embedded soundtouch-player surface // (/api/control + /app); discovery is nil since we only register routes. server := handlers.NewServer(nil, nil, "http://localhost:8000", true, true, true) r := setupRouter(server, nil, soundtouchweb.NewWebApp()) diff --git a/cmd/soundtouch-web/.gitignore b/cmd/soundtouch-web/.gitignore deleted file mode 100644 index 2f12af7..0000000 --- a/cmd/soundtouch-web/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -soundtouch-web -soundtouch-web-test diff --git a/docs/archive/PLAN.md b/docs/archive/PLAN.md index f29bb79..9be6a2c 100644 --- a/docs/archive/PLAN.md +++ b/docs/archive/PLAN.md @@ -665,7 +665,7 @@ soundtouch --device 192.0.2.100 preset 1 soundtouch interactive # Web interface -soundtouch-webapp --port 8080 +soundtouch-playerapp --port 8080 ``` ### JavaScript/WASM Usage @@ -727,10 +727,10 @@ client.startEventStream((event) => { ./soundtouch-linux-amd64 --device IP play # Web Application (embedded assets) -./soundtouch-webapp-linux-amd64 --port 8080 +./soundtouch-playerapp-linux-amd64 --port 8080 # Docker -docker run -p 8080:8080 soundtouch-webapp +docker run -p 8080:8080 soundtouch-playerapp ``` ### Development Environment diff --git a/docs/content/_index.md b/docs/content/_index.md index 48d2f6e..8fa6fc4 100644 --- a/docs/content/_index.md +++ b/docs/content/_index.md @@ -35,7 +35,7 @@ layout: hextra-home >}} {{< hextra/feature-card title="Music Browsing" - subtitle="TuneIn, Internet Radio, RadioBrowser, and Spotify via soundtouch-web and soundtouch-cli." + subtitle="TuneIn, Internet Radio, RadioBrowser, and Spotify via soundtouch-player and soundtouch-cli." icon="speakerphone" >}} {{< hextra/feature-card diff --git a/docs/content/blog/2026-05-welcome.md b/docs/content/blog/2026-05-welcome.md index f2becb5..9ad4a73 100644 --- a/docs/content/blog/2026-05-welcome.md +++ b/docs/content/blog/2026-05-welcome.md @@ -46,16 +46,16 @@ selection behave the same as before. Your six preset buttons work. AfterTouch stores preset bindings locally and serves them back to the speaker on request. You can also **save new presets** β€” via the API, -via `soundtouch-cli`, or through the soundtouch-web UI. +via `soundtouch-cli`, or through the soundtouch-player UI. ### ST-10 stereo pairing **SoundTouch 10 stereo pairs** (and other ST pairing configurations) are supported end-to-end: creation, management, and playback routing all go through AfterTouch. -### soundtouch-web β€” browser UI +### soundtouch-player β€” browser UI -**soundtouch-web** is an early-stage but functional browser UI bundled with AfterTouch. +**soundtouch-player** is an early-stage but functional browser UI bundled with AfterTouch. It gives you: - TuneIn and RadioBrowser browsing and playback @@ -65,7 +65,7 @@ It gives you: It runs as part of the AfterTouch service β€” no separate install needed. -![soundtouch-web UI showing Spotify playback, presets, sources, and zone management](/images/blog/soundtouch-web-ui.png) +![soundtouch-player UI showing Spotify playback, presets, sources, and zone management](/images/blog/soundtouch-player-ui.png) ### Automation with soundtouch-cli @@ -110,9 +110,9 @@ right places to start. ## What's next -The soundtouch-web UI will gain richer preset management β€” browsing, editing, and +The soundtouch-player UI will gain richer preset management β€” browsing, editing, and reordering presets directly from the browser. Longer term, merging -`soundtouch-service` and `soundtouch-web` into a single binary is on the table, +`soundtouch-service` and `soundtouch-player` into a single binary is on the table, which would simplify deployment to a single process with no extra flags. This blog will be updated monthly β€” or whenever something significant ships. diff --git a/docs/content/docs/analysis/bose-soundtouch-community-tools.md b/docs/content/docs/analysis/bose-soundtouch-community-tools.md index 116bd42..9b41cae 100644 --- a/docs/content/docs/analysis/bose-soundtouch-community-tools.md +++ b/docs/content/docs/analysis/bose-soundtouch-community-tools.md @@ -129,8 +129,8 @@ A Java-based middleware that hosts the original Bose Stockholm frontend (extract --- -### 8. jaas666/bose-soundtouch-web-api (Reference) -**[github.com/jaas666/bose-soundtouch-web-api](https://github.com/jaas666/bose-soundtouch-web-api)** +### 8. jaas666/bose-soundtouch-player-api (Reference) +**[github.com/jaas666/bose-soundtouch-player-api](https://github.com/jaas666/bose-soundtouch-player-api)** Community-maintained Markdown conversion of the official Bose SoundTouch Web API PDF (v1.0, January 7, 2026). Useful as a developer reference. Not a deployable tool. @@ -198,7 +198,7 @@ The web UI is already fully responsive β€” it has Bootstrap grid columns, `@medi ### Priority 2 β€” RadioBrowser as a first-class provider AfterTouch can proxy and play any stream URL, but there is no built-in station search. OpenCloudTouch's RadioBrowser integration is the reference. Tasks: -- Wire the [RadioBrowser API](https://www.radio-browser.info/) into the `soundtouch-web` web UI as a browsable/searchable source. +- Wire the [RadioBrowser API](https://www.radio-browser.info/) into the `soundtouch-player` web UI as a browsable/searchable source. - Make discovered stations directly presetable to hardware buttons. - This is the most common replacement for TuneIn for users who listened to internet radio via presets. @@ -242,7 +242,7 @@ These exist in soundcork but are deliberate architectural choices in AfterTouch, | Area | soundcork | AfterTouch | |--------------------------|---------------------------------------|-----------------------------------------------------------| -| Web UI | FastAPI + Jinja2 miniapp and admin UI | Separate `soundtouch-web` component (Go + plain HTML/JS) | +| Web UI | FastAPI + Jinja2 miniapp and admin UI | Separate `soundtouch-player` component (Go + plain HTML/JS) | | Direct device management | SSH/SCP access into speakers | HTTP API only; no SSH | | Device discovery client | Python `upnpclient` library | mDNS + UPnP in Go, with dedicated DNS interception server | | Token delivery | Push (ZeroConf priming to port 8200) | Pull (device calls back to fetch) | @@ -288,7 +288,7 @@ Bose's decision to release API documentation rather than simply shutting down is - [Bose SoundTouch Plus (Home Assistant component)](https://github.com/thlucas1/homeassistantcomponent_soundtouchplus) β€” comprehensive HA integration by Todd Lucas, extensive API wiki - [Bose SoundTouch Hook](https://github.com/CodeFinder2/bose-soundtouch-hook) β€” `LD_PRELOAD`-based reverse engineering framework used by AfterTouch for protocol research -- [Bose SoundTouch Web API (community Markdown)](https://github.com/jaas666/bose-soundtouch-web-api) β€” official API PDF converted to Markdown +- [Bose SoundTouch Web API (community Markdown)](https://github.com/jaas666/bose-soundtouch-player-api) β€” official API PDF converted to Markdown - [Bose Wiki β€” SoundTouch App Alternatives](https://bose.fandom.com/wiki/SoundTouch_app_alternatives) β€” community-maintained living list of workarounds and projects - [Reddit megathread β€” Bose alternatives](https://www.reddit.com/r/bose) β€” ongoing community discussion - [Radio Browser](https://www.radio-browser.info/) β€” the free, community-maintained internet radio directory used as a TuneIn replacement diff --git a/docs/content/docs/appendix/PRESET-QUICKSTART.md b/docs/content/docs/appendix/PRESET-QUICKSTART.md index de40408..d5c1a2a 100644 --- a/docs/content/docs/appendix/PRESET-QUICKSTART.md +++ b/docs/content/docs/appendix/PRESET-QUICKSTART.md @@ -7,11 +7,11 @@ sidebar: ## Overview -SoundTouch devices support 6 preset slots that can store your favorite content for instant access. This guide shows you how to manage presets using the soundtouch-web UI, the CLI, or the Go library. +SoundTouch devices support 6 preset slots that can store your favorite content for instant access. This guide shows you how to manage presets using the soundtouch-player UI, the CLI, or the Go library. -## Via soundtouch-web (browser UI) +## Via soundtouch-player (browser UI) -**soundtouch-web** (default port **8080**) is the easiest way to manage presets without the command line. Two save paths are available whenever content is playing: +**soundtouch-player** (default port **8080**) is the easiest way to manage presets without the command line. Two save paths are available whenever content is playing: ### β˜… Star button β€” save from Now Playing diff --git a/docs/content/docs/appendix/soundtouch-web-roadmap.md b/docs/content/docs/appendix/soundtouch-player-roadmap.md similarity index 95% rename from docs/content/docs/appendix/soundtouch-web-roadmap.md rename to docs/content/docs/appendix/soundtouch-player-roadmap.md index 7c1d783..9219206 100644 --- a/docs/content/docs/appendix/soundtouch-web-roadmap.md +++ b/docs/content/docs/appendix/soundtouch-player-roadmap.md @@ -1,9 +1,9 @@ --- -title: "soundtouch-web: remaining features" +title: "soundtouch-player: remaining features" sidebar: exclude: true --- -Four features complete the parity gap between soundtouch-web and the Stockholm +Four features complete the parity gap between soundtouch-player and the Stockholm app's local-control functionality. Everything else in Stockholm (OAuth flows, setup wizard, service account linking, onboarding, analytics) is cloud infrastructure that is either shut down or already handled by soundtouch-service. @@ -48,7 +48,7 @@ func (c *Client) Seek(positionSeconds int) error { > **Note:** This section is about the speaker's **built-in** `/favorites` API β€” > a separate concept from the 6 preset slots. Preset-slot saving (β˜… star / > **+** button) is already shipped; the native Favorites API is not yet -> surfaced in soundtouch-web. +> surfaced in soundtouch-player. Mark or unmark the currently playing track as a device favourite directly from the Now Playing card. Unlike presets (maximum 6, numbered slots), the device @@ -103,7 +103,7 @@ rename and network/firmware info. ## 4. Render stereo pairs as a single device -Today soundtouch-web shows the two halves of a stereo pair (formed via +Today soundtouch-player shows the two halves of a stereo pair (formed via `/addGroup` β€” see issue #252) as independent entries in the device list. The Bose app collapsed a paired ST10 set into one "L+R" entry; restoring that presentation closes the perception gap BirdyBA flagged at @@ -139,7 +139,7 @@ end-to-end β€” `pkg/client` group endpoints + `cmd/soundtouch-cli/cmd_group.go`, covered by tests in `cmd/soundtouch-cli/cmd_group_test.go` and exercisable against the fake speaker's group routes (`pkg/service/testing/fakespeaker/fakespeaker.go`). This task is purely about -presentation in soundtouch-web's device list β€” no protocol work required. +presentation in soundtouch-player's device list β€” no protocol work required. --- diff --git a/docs/content/docs/architecture/API-ROUTE-LAYOUT.md b/docs/content/docs/architecture/API-ROUTE-LAYOUT.md index d736b8f..5c5e074 100644 --- a/docs/content/docs/architecture/API-ROUTE-LAYOUT.md +++ b/docs/content/docs/architecture/API-ROUTE-LAYOUT.md @@ -2,17 +2,17 @@ title: "API Route Layout and Refactoring Plan" --- -> **Tracking issue:** [#451 "Merge soundtouch-web into soundtouch-service"](https://github.com/gesellix/Bose-SoundTouch/issues/451). +> **Tracking issue:** [#451 "Merge soundtouch-player into soundtouch-service"](https://github.com/gesellix/Bose-SoundTouch/issues/451). > This document is the architectural reference for the staged API refactoring > that precedes (and enables) that merge. ## Why this exists -`soundtouch-service` and `soundtouch-web` are two binaries with two routers. +`soundtouch-service` and `soundtouch-player` are two binaries with two routers. We want to: 1. Restructure our own routes into a layout that can stay stable. -2. Eventually fold `soundtouch-web` into `soundtouch-service` (one binary). +2. Eventually fold `soundtouch-player` into `soundtouch-service` (one binary). 3. Stop leaking frontend (SPA) routes into the backend API. 4. Make **cloud / remote-host a first-class, clean deployment**, not just LAN / on-device. This is a primary motivation: we consolidate the API *in a way @@ -34,7 +34,7 @@ Classify by client audience, then by what pins the path: | **(1a) Frozen, firmware-pinned** | Speaker firmware | No, ever. The path is hardcoded in the speaker (or relative to a base it fetches from us). | | **(1b) Frozen, externally-pinned** | OAuth providers (Spotify/Amazon) | Only with provider re-registration + device re-priming. Treat as frozen unless that cost is paid deliberately. | | **(2) Service-internal** | The admin/setup UI | Yes, freely. These are ours. | -| **(3) Web/control** | The control UI (soundtouch-web) | Yes, freely. | +| **(3) Web/control** | The control UI (soundtouch-player) | Yes, freely. | | **(4) Frontend (SPA)** | Browser, client-side routing | Should not be enumerated in the backend at all (see `/app/*` below). | | **(Infra)** | Humans, monitoring, the SPA shell | Conventionally stable; collision-prone at merge time. | @@ -103,7 +103,7 @@ Grouped by prefix. The authoritative enumerated list is the router golden file | `/web/*` (`HandleWeb`) | (4) frontend | Browser (admin SPA) | Yes; already the clean catch-all pattern | | `/`
`/docs/*`
`/favicon.ico`
`/health` | (Infra) | Humans / monitoring | Keep stable by convention | -## Web routes (`soundtouch-web`) +## Web routes (`soundtouch-player`) Defined in `pkg/service/soundtouchweb/mount.go`. Not currently mounted inside the service; it is a separate binary. @@ -166,7 +166,7 @@ deployments: - **Speaker-direct (control plane):** the service opens a connection *to* the speaker's local API (`:8090`) right now. Discovery, migration, reboot, - test-connection, peer-probe, and the entire `soundtouch-web` + test-connection, peer-probe, and the entire `soundtouch-player` control/zone/volume/key/TTS-to-speaker surface. These only work where the host shares the LAN with the speaker. **In a cloud deployment they are dead weight**, and any UI that shows them is misleading. @@ -386,7 +386,7 @@ diagnostic export, not leak into shared bundles. # auth/authz middleware applies per group, not per route): /api/setup/* (today: /setup/*) -> admin tier: auth required /api/mgmt/* (today: /mgmt/*, no callbacks) -> admin tier: auth required -/api/control/* (today: soundtouch-web /api/*) -> player tier: auth optional +/api/control/* (today: soundtouch-player /api/*) -> player tier: auth optional /api/devices ... # OAuth provider callbacks (externally-pinned; freeze in place, @@ -404,7 +404,7 @@ diagnostic export, not leak into shared bundles. ### The `/app/*` pattern The service's admin UI already does the right thing: `/web/*` is one catch-all -(`HandleWeb`), not one route per page. The `soundtouch-web` SPA routes +(`HandleWeb`), not one route per page. The `soundtouch-player` SPA routes (`mount.go`, the `/`, `/devices`, `/tunein`, ... block) are the legacy anti-pattern. The target: @@ -523,7 +523,7 @@ them. step is about our movable routes only. - **Exclude** `/mgmt/spotify/callback` and `/mgmt/amazon/callback` (1b): freeze, or move only with a deliberate provider re-registration. -2. **First, migrate `soundtouch-web` in place to the target API shape.** Before +2. **First, migrate `soundtouch-player` in place to the target API shape.** Before touching the service, restructure the standalone `-web` binary's own routes to what they should be *after* the merge: the control API under `/api/control/*` and the SPA under `/app/*` (with `/ws` as e.g. `/api/control/ws`). Unlike the @@ -537,7 +537,7 @@ them. routes already match the target and don't overlap the service's namespaces, so the merge below is a near-additive mount. -3. **Fold `soundtouch-web` into the service.** Bring the (already target-shaped) +3. **Fold `soundtouch-player` into the service.** Bring the (already target-shaped) control API in as `/api/control/*` and the UI under `/app/*` (one role-gated app, see above). The actual overlap to clean up (verified) is small: only **`/`** truly @@ -556,7 +556,7 @@ them. surface from being exposed unless an operator deliberately enables it. The flag follows the same CLI/env/persisted precedence as `server-url`, and is the seam the `deployment-mode` parameter later subsumes. -4. **Deprecate the `soundtouch-web` binary.** It keeps working in 0.x but prints +4. **Deprecate the `soundtouch-player` binary.** It keeps working in 0.x but prints a startup deprecation warning (along the lines of "this binary is removed in 1.x, use soundtouch-service") so its removal is no surprise. 5. **Warn on old-route hits in the service, observably.** When a deprecated path @@ -597,7 +597,7 @@ in a 0.x release first: ### 1.x cutover -Remove the obsolete routes and retire `soundtouch-web`. Per the versioning +Remove the obsolete routes and retire `soundtouch-player`. Per the versioning section, this is the only point where anything is removed; the frozen speaker/app routes stay. diff --git a/docs/content/docs/architecture/DEVICE-LOCAL-INSTALL.md b/docs/content/docs/architecture/DEVICE-LOCAL-INSTALL.md index 88d1c9f..40e1644 100644 --- a/docs/content/docs/architecture/DEVICE-LOCAL-INSTALL.md +++ b/docs/content/docs/architecture/DEVICE-LOCAL-INSTALL.md @@ -156,7 +156,7 @@ Where today's surfaces fall short for this user: **Goal.** Music plays. Pressing preset 3 gives them what preset 3 should give them. Skipping a station, adjusting volume, browsing for a new station β€” all fast, no friction. -**Surfaces.** Physical preset buttons (always there), `soundtouch-web` (today), mobile app (Journey 2 admin app's daily-use mode), WASM-served browser UI (planned), Bose app while it still functions, voice assistants where wired up. +**Surfaces.** Physical preset buttons (always there), `soundtouch-player` (today), mobile app (Journey 2 admin app's daily-use mode), WASM-served browser UI (planned), Bose app while it still functions, voice assistants where wired up. ### What this layer needs to be good at @@ -168,14 +168,14 @@ Where today's surfaces fall short for this user: ### How surfaces map -- `soundtouch-web`: primary daily UI for desktop browsers and (responsively) for tablets. This is already shipped. +- `soundtouch-player`: primary daily UI for desktop browsers and (responsively) for tablets. This is already shipped. - Mobile app: daily-use mode of the same Gio app that handles admin. Capability split β€” admin features only show up when the user is in admin mode. - WASM: same Gio app, served from `soundtouch-service` to anyone on the LAN. The "I forgot which device my login is on, just open a browser" fallback. - Physical preset buttons: handled at the agent level (the Bose firmware fires them; AfterTouch or the on-device agent reacts). ### Open decisions for this journey -- Do we keep `soundtouch-web` as a separate codebase (HTML/JS), or does it become a Gio WASM build sharing code with the admin app? +- Do we keep `soundtouch-player` as a separate codebase (HTML/JS), or does it become a Gio WASM build sharing code with the admin app? - Mobile app store distribution: TestFlight for iOS (gated, slow), Play Store for Android (faster, AAB only), F-Droid as an open-source-friendly side path. - Multi-user state: presets per-user vs per-household. Out of scope here, but the daily surface is where it gets felt. @@ -199,7 +199,7 @@ Where today's surfaces fall short for this user: ### How surfaces map - `soundtouch-cli`: the canonical surface for scripted control. Already covers most of the API. -- `soundtouch-service` REST endpoints: same surface, network-accessible. Used by `soundtouch-web` and by third-party automation. +- `soundtouch-service` REST endpoints: same surface, network-accessible. Used by `soundtouch-player` and by third-party automation. - Home Assistant: external integration; track but do not own. - Webhooks / MQTT: not present today; would let speakers participate in event-driven flows. Out of scope for a first pass; worth a separate design doc when demand surfaces. @@ -217,7 +217,7 @@ Where today's surfaces fall short for this user: |------------------------------------|---------------------|-------------------|-------------------|------------------------| | `soundtouch-cli` | partial (today) | partial (today) | no | primary | | `soundtouch-service` web UI | wizard portion | primary | partial | indirect (REST) | -| `soundtouch-web` | no | no | primary | no | +| `soundtouch-player` | no | no | primary | no | | GUI admin app (Gio, planned) | primary | primary | mobile mode | no | | Pre-flashed stick (hypothetical) | primary | recovery | no | no | | Physical preset buttons | no | no | primary | no | @@ -229,7 +229,7 @@ The diagonal isn't full because some journeys lack a polished surface today (Jou The Gio admin app, if built, can target Windows / macOS / Linux / iOS / Android / WASM from one codebase. Each target has hard constraints: -- **WASM (browser).** Post-install REST control, device list and status, preset editing, station search. No mDNS (browsers cannot do raw multicast β€” fall back to manual IP entry or a backend bridge); no raw TCP, so no SSH and no install; no block-device access, so no stick writing. This is the "I just want to use my speakers" surface, equivalent to today's `soundtouch-web`. +- **WASM (browser).** Post-install REST control, device list and status, preset editing, station search. No mDNS (browsers cannot do raw multicast β€” fall back to manual IP entry or a backend bridge); no raw TCP, so no SSH and no install; no block-device access, so no stick writing. This is the "I just want to use my speakers" surface, equivalent to today's `soundtouch-player`. - **Mobile iOS.** Everything WASM does, plus Bonjour-based mDNS, plus full SSH client (so app-driven install and recovery work). No FAT32 stick writing β€” iOS has no filesystem-level block device access for third-party apps. Best paired with a pre-flashed stick or a friend's desktop install for the bootstrap. - **Mobile Android.** Same as iOS, plus FAT32 stick writing *if* the user grants USB-OTG host permission. UX caveat: most users will not know what USB host mode is. - **Desktop (Gio).** Full capability set. mDNS, SSH-driven install, FAT32 stick writing via standard block-device APIs, post-install control, recovery. The primary onboarding surface. diff --git a/docs/content/docs/architecture/_index.md b/docs/content/docs/architecture/_index.md index 1adba74..ee24f72 100644 --- a/docs/content/docs/architecture/_index.md +++ b/docs/content/docs/architecture/_index.md @@ -8,6 +8,6 @@ Architecture notes and analyses: - [API Route Layout and Refactoring Plan](API-ROUTE-LAYOUT.md) - route classification (frozen speaker contract vs our movable surface), the actor / deployment / trust model, auth, and the staged plan toward the - `soundtouch-web` / `soundtouch-service` merge (issue #451). + `soundtouch-player` / `soundtouch-service` merge (issue #451). - [Device-Local Install: Four User Journeys](DEVICE-LOCAL-INSTALL.md) - install patterns and user journeys for on-device deployment. diff --git a/docs/content/docs/guides/EXTERNAL-HOST-WALKTHROUGH.md b/docs/content/docs/guides/EXTERNAL-HOST-WALKTHROUGH.md index 0bd0621..6392c54 100644 --- a/docs/content/docs/guides/EXTERNAL-HOST-WALKTHROUGH.md +++ b/docs/content/docs/guides/EXTERNAL-HOST-WALKTHROUGH.md @@ -167,16 +167,16 @@ curl -s http://192.0.2.1:8090/sources ## Step 7 β€” Set up preset buttons (optional) -### Via soundtouch-web +### Via soundtouch-player The Radio Browser, TuneIn tabs, and preset saving live in -**soundtouch-web**, a separate binary from the service. Once running, +**soundtouch-player**, a separate binary from the service. Once running, open **`http://:8080`** in your browser (default port 8080). -### Installing soundtouch-web on a Raspberry Pi +### Installing soundtouch-player on a Raspberry Pi `install.sh` only installs `soundtouch-service`. Use the dedicated -`install-web.sh` script to add soundtouch-web: +`install-web.sh` script to add soundtouch-player: ```bash curl -fsSL -o install-web.sh \ @@ -185,22 +185,22 @@ sudo bash install-web.sh ``` For configuration, service management, updates, and removal see the -[Raspberry Pi guide β†’ soundtouch-web](RASPBERRY-PI.md#soundtouch-web). +[Raspberry Pi guide β†’ soundtouch-player](RASPBERRY-PI.md#soundtouch-player). -### Installing soundtouch-web on other hosts +### Installing soundtouch-player on other hosts Download the binary for your OS and architecture from the [Releases page](https://github.com/gesellix/Bose-SoundTouch/releases) and run it directly: ```bash -./soundtouch-web --port 8080 +./soundtouch-player --port 8080 ``` Or install it as a systemd service following the same unit-file pattern described in [DEPLOYMENT.md](DEPLOYMENT.md). -soundtouch-web provides two ways to save what's currently playing to a +soundtouch-player provides two ways to save what's currently playing to a preset slot: **β˜… Star button in the Now Playing card** diff --git a/docs/content/docs/guides/RASPBERRY-PI.md b/docs/content/docs/guides/RASPBERRY-PI.md index 655d9d4..338ae5f 100644 --- a/docs/content/docs/guides/RASPBERRY-PI.md +++ b/docs/content/docs/guides/RASPBERRY-PI.md @@ -9,7 +9,7 @@ Two scripts are available, one per binary: | Script | Binary | Role | Default port | |------------------|----------------------|-------------------------------------|--------------| | `install.sh` | `soundtouch-service` | Cloud-replacement relay β€” always-on | 80 / 443 | -| `install-web.sh` | `soundtouch-web` | Browser control panel | 8080 | +| `install-web.sh` | `soundtouch-player` | Browser control panel | 8080 | Both auto-detect CPU architecture (armv7 / arm64 / amd64), create a `soundtouch` system user, and install a systemd unit. They are safe to re-run for updates. @@ -118,9 +118,9 @@ sudo systemctl daemon-reload --- -## soundtouch-web +## soundtouch-player -`soundtouch-web` is a stateless browser control panel β€” it holds no persistent +`soundtouch-player` is a stateless browser control panel β€” it holds no persistent data and can be stopped or restarted at any time without data loss. ### Installation @@ -151,7 +151,7 @@ Once running, open **`http://:8080`** in a browser. ### Configuration ``` -/etc/soundtouch-web/soundtouch-web.env +/etc/soundtouch-player/soundtouch-player.env ``` Example: @@ -173,7 +173,7 @@ network: SOUNDTOUCH_DEVICES=192.0.2.1,192.0.2.2 ``` -`SERVICE_URL` links `soundtouch-web` to your `soundtouch-service` instance, +`SERVICE_URL` links `soundtouch-player` to your `soundtouch-service` instance, which is required for Text-to-Speech ("Speak"). When the service is served over HTTPS with its own self-signed certificate (the default), also set `SERVICE_CA` to that CA certificate, or the proxied TTS call fails with @@ -192,7 +192,7 @@ be left empty. After editing the env file: ```bash -sudo systemctl restart soundtouch-web +sudo systemctl restart soundtouch-player ``` ### Port conflicts @@ -210,19 +210,19 @@ the env file after installation and restart the service. ### Service management ```bash -systemctl status soundtouch-web -sudo systemctl enable soundtouch-web # start on boot -sudo systemctl disable soundtouch-web -sudo systemctl stop soundtouch-web -sudo systemctl start soundtouch-web -sudo systemctl restart soundtouch-web +systemctl status soundtouch-player +sudo systemctl enable soundtouch-player # start on boot +sudo systemctl disable soundtouch-player +sudo systemctl stop soundtouch-player +sudo systemctl start soundtouch-player +sudo systemctl restart soundtouch-player ``` ### Logs ```bash -journalctl -u soundtouch-web -e --no-pager -journalctl -u soundtouch-web -f +journalctl -u soundtouch-player -e --no-pager +journalctl -u soundtouch-player -f ``` ### Updates @@ -235,10 +235,10 @@ sudo bash install-web.sh v0.107.0 # update to a specific version ### Removal ```bash -sudo systemctl disable --now soundtouch-web -sudo rm /etc/systemd/system/soundtouch-web.service -sudo rm -rf /etc/soundtouch-web -sudo rm /usr/local/bin/soundtouch-web +sudo systemctl disable --now soundtouch-player +sudo rm /etc/systemd/system/soundtouch-player.service +sudo rm -rf /etc/soundtouch-player +sudo rm /usr/local/bin/soundtouch-player sudo systemctl daemon-reload ``` diff --git a/docs/content/docs/guides/SOUNDTOUCH-SERVICE.md b/docs/content/docs/guides/SOUNDTOUCH-SERVICE.md index 211aaaf..4428c52 100644 --- a/docs/content/docs/guides/SOUNDTOUCH-SERVICE.md +++ b/docs/content/docs/guides/SOUNDTOUCH-SERVICE.md @@ -30,7 +30,7 @@ The service consists of several key components: - **Service Registry**: Media service discovery and configuration - **Playback Control**: Stream URL resolution and audio metadata -![soundtouch-web TuneIn search β€” browsing smooth jazz stations](/images/soundtouch-web-tunein.png) +![soundtouch-player TuneIn search β€” browsing smooth jazz stations](/images/soundtouch-player-tunein.png) ### Marge Services (Account & Device Management) - **Account Management**: User account simulation and device association diff --git a/docs/content/docs/guides/SURVIVAL-GUIDE.md b/docs/content/docs/guides/SURVIVAL-GUIDE.md index b49a0a2..9af622e 100644 --- a/docs/content/docs/guides/SURVIVAL-GUIDE.md +++ b/docs/content/docs/guides/SURVIVAL-GUIDE.md @@ -11,14 +11,14 @@ Bose shut down SoundTouch cloud services on **May 6, 2026**. Per the [official e What **continues to work** regardless: - The official SoundTouch app for local control (play/pause/volume/source selection) -- Local playback controls via `soundtouch-cli`, `soundtouch-web`, or any app that uses the local Web API +- Local playback controls via `soundtouch-cli`, `soundtouch-player`, or any app that uses the local Web API - Bluetooth, AUX, and AirPlay inputs - Multiroom zones (local, peer-to-peer) **AfterTouch** β€” the `soundtouch-service` β€” restores the first three: - **Presets** β€” full preset management including long-press assignment and recently-played sync; music service presets (Spotify, TuneIn, etc.) work once the service is linked (see [Connecting Music Services](MUSIC-SERVICES.md)) -- **Music browsing and playback** β€” TuneIn, Internet Radio, and RadioBrowser via `soundtouch-web`; direct station/URL playback via `soundtouch-cli`; Spotify via Spotify Connect (speaker-native) or AfterTouch's OAuth integration; Amazon Music OAuth infrastructure is in place but streaming is not yet verified +- **Music browsing and playback** β€” TuneIn, Internet Radio, and RadioBrowser via `soundtouch-player`; direct station/URL playback via `soundtouch-cli`; Spotify via Spotify Connect (speaker-native) or AfterTouch's OAuth integration; Amazon Music OAuth infrastructure is in place but streaming is not yet verified - **Stereo pairing** β€” via `soundtouch-cli` Alexa voice commands are not currently supported. diff --git a/docs/content/docs/guides/TROUBLESHOOTING.md b/docs/content/docs/guides/TROUBLESHOOTING.md index 77d57e3..9a9f98f 100644 --- a/docs/content/docs/guides/TROUBLESHOOTING.md +++ b/docs/content/docs/guides/TROUBLESHOOTING.md @@ -623,7 +623,7 @@ fmt.Printf("Current source: %s, status: %s\n", nowPlaying.Source, nowPlaying.PlayStatus) ``` -### ❌ soundtouch-web TTS fails with `certificate signed by unknown authority` +### ❌ soundtouch-player TTS fails with `certificate signed by unknown authority` **Symptoms:** ``` @@ -632,17 +632,17 @@ tls: failed to verify certificate: x509: certificate signed by unknown authority ``` **Cause:** TTS synthesis and the Bose app key live in `soundtouch-service`, -so `soundtouch-web` proxies the "Speak" action to the service. When the +so `soundtouch-player` proxies the "Speak" action to the service. When the service is served over HTTPS with its own self-signed certificate (the -default β€” see `GET /setup/ca.crt`), `soundtouch-web` doesn't trust that CA out +default β€” see `GET /setup/ca.crt`), `soundtouch-player` doesn't trust that CA out of the box, so the proxied call fails verification. -**Solution:** start `soundtouch-web` with `--service-ca` pointing at the +**Solution:** start `soundtouch-player` with `--service-ca` pointing at the service's CA certificate (its `/certs/ca.crt`, or the file served at `/setup/ca.crt`): ```bash -soundtouch-web \ +soundtouch-player \ --service-url https://soundtouch.fritz.box \ --service-ca /path/to/certs/ca.crt ``` @@ -651,7 +651,7 @@ soundtouch-web \ system trust store, so a service URL that uses a publicly trusted certificate needs no flag. -### ❌ soundtouch-web TTS returns `host ... is not a known device` +### ❌ soundtouch-player TTS returns `host ... is not a known device` **Symptoms:** ``` @@ -664,8 +664,8 @@ verbatim from the request). **Solution:** make sure the target speaker is known to `soundtouch-service` (discovered or manually added, and migrated to AfterTouch), not only to -`soundtouch-web`'s own discovery. Check with `GET /setup/devices` on the -service. (Recent `soundtouch-web` versions identify the speaker by its device +`soundtouch-player`'s own discovery. Check with `GET /setup/devices` on the +service. (Recent `soundtouch-player` versions identify the speaker by its device ID and a bare IP, so this error otherwise indicates the speaker simply isn't registered with the service.) diff --git a/docs/content/docs/reference/SPEAKER-ENDPOINT.md b/docs/content/docs/reference/SPEAKER-ENDPOINT.md index cc1389c..839ca30 100644 --- a/docs/content/docs/reference/SPEAKER-ENDPOINT.md +++ b/docs/content/docs/reference/SPEAKER-ENDPOINT.md @@ -333,7 +333,7 @@ soundtouch-cli speaker tts-cloud \ --method speaker ``` -Web UI: the TTS source view has a "Say something…" box. soundtouch-web proxies +Web UI: the TTS source view has a "Say something…" box. soundtouch-player proxies it to the service, so it must be started with `--service-url` (the target is server-configured, not entered in the browser, to avoid an SSRF proxy). diff --git a/docs/static/images/blog/soundtouch-web-ui.png b/docs/static/images/blog/soundtouch-player-ui.png similarity index 100% rename from docs/static/images/blog/soundtouch-web-ui.png rename to docs/static/images/blog/soundtouch-player-ui.png diff --git a/docs/static/images/soundtouch-web-tunein.png b/docs/static/images/soundtouch-player-tunein.png similarity index 100% rename from docs/static/images/soundtouch-web-tunein.png rename to docs/static/images/soundtouch-player-tunein.png diff --git a/pkg/service/handlers/handlers_tts_test.go b/pkg/service/handlers/handlers_tts_test.go index 42f50fe..965b316 100644 --- a/pkg/service/handlers/handlers_tts_test.go +++ b/pkg/service/handlers/handlers_tts_test.go @@ -184,7 +184,7 @@ func TestHandleTTSSpeakValidation(t *testing.T) { } // TestResolveTTSHost covers the SSRF guard plus the host-normalization that -// lets a base URL (what a client's Host() returns, e.g. soundtouch-web sends +// lets a base URL (what a client's Host() returns, e.g. soundtouch-player sends // "http://ip:8090") resolve to a known device. The result is always the // datastore's bare IP, never the caller's value. func TestResolveTTSHost(t *testing.T) { diff --git a/pkg/service/soundtouchweb/discovery.go b/pkg/service/soundtouchweb/discovery.go index dccbde9..e7df40b 100644 --- a/pkg/service/soundtouchweb/discovery.go +++ b/pkg/service/soundtouchweb/discovery.go @@ -95,7 +95,7 @@ func (app *WebApp) AddDeviceByHost(host string, port int, source string) { // SeedExtraDevices registers any devices reported by the ExtraDeviceHosts hook // (if set) via AddDeviceByHost. Idempotent: already-known hosts are skipped. // Used by the embedded build to surface the service datastore's devices even -// when network discovery is disabled; a no-op for standalone soundtouch-web. +// when network discovery is disabled; a no-op for standalone soundtouch-player. // // Hosts are probed concurrently: AddDeviceByHost makes a blocking /info call // (up to its 10 s timeout) for each unknown host, so an offline speaker in the @@ -130,7 +130,7 @@ func (app *WebApp) SeedExtraDevices() { // (embedded build), it runs the host service's discovery so the shared store is // refreshed, then re-syncs from ExtraDeviceHosts β€” it does NOT run its own // mDNS/UPnP, so the embedded build never duplicates the service's discovery. -// When discoveryService is non-nil (standalone soundtouch-web), it runs an +// When discoveryService is non-nil (standalone soundtouch-player), it runs an // mDNS/UPnP sweep and registers any found devices via AddDeviceByHost. // Used by the startup goroutine in main and by the /api/control/discover route // inside MountWeb. diff --git a/pkg/service/soundtouchweb/handler.go b/pkg/service/soundtouchweb/handler.go index 12df157..67a8f89 100644 --- a/pkg/service/soundtouchweb/handler.go +++ b/pkg/service/soundtouchweb/handler.go @@ -53,20 +53,20 @@ type WebApp struct { // register alongside mDNS/UPnP discovery. The embedded build in // soundtouch-service points it at the service datastore's known devices so // the UI shows manually-added speakers even when network discovery is - // disabled. Standalone soundtouch-web leaves it nil. + // disabled. Standalone soundtouch-player leaves it nil. ExtraDeviceHosts func() []string // TriggerDiscovery, when set, runs an external discovery sweep instead of // this app's own mDNS/UPnP. The embedded build wires it to the host // service's discovery (the single source of truth, which updates the shared // datastore); DiscoverDevices then re-syncs from ExtraDeviceHosts. Standalone - // soundtouch-web leaves it nil and runs its own sweep. + // soundtouch-player leaves it nil and runs its own sweep. TriggerDiscovery func(ctx context.Context) // RemoveDeviceHook, when set, removes a device from the backing store by // its device ID (MAC). The embedded build wires it to the service's // datastore removal so a removal from the player UI also clears the - // persisted device; standalone soundtouch-web leaves it nil (no store, so + // persisted device; standalone soundtouch-player leaves it nil (no store, so // removal only prunes the in-memory registry). RemoveDeviceHook func(deviceID string) error @@ -277,7 +277,7 @@ func (app *WebApp) HandleDeleteDevice(w http.ResponseWriter, r *http.Request) { } // Cascade to the backing store (embedded build only). Standalone - // soundtouch-web has no datastore and leaves the hook nil, so removal + // soundtouch-player has no datastore and leaves the hook nil, so removal // only prunes the in-memory registry below. if app.RemoveDeviceHook != nil { deviceID := "" @@ -1206,7 +1206,7 @@ func (app *WebApp) HandlePlayURL(w http.ResponseWriter, r *http.Request) { if serviceURL == "" { app.sendError(w, "AfterTouch service URL is required for LOCAL_INTERNET_RADIO playback. "+ - "Start soundtouch-web with --service-url or enter it in the Play URL settings.", + "Start soundtouch-player with --service-url or enter it in the Play URL settings.", http.StatusBadRequest) return diff --git a/pkg/service/soundtouchweb/handlers_tts.go b/pkg/service/soundtouchweb/handlers_tts.go index 9c00258..fa03e7f 100644 --- a/pkg/service/soundtouchweb/handlers_tts.go +++ b/pkg/service/soundtouchweb/handlers_tts.go @@ -70,15 +70,15 @@ func (app *WebApp) HandleAPISpeakText(w http.ResponseWriter, r *http.Request) { return } - // The TTS request is made server-side by soundtouch-web, so its target must + // The TTS request is made server-side by soundtouch-player, so its target must // be the operator-configured service URL β€” never a client-supplied value // (that would let any LAN caller use this endpoint as an SSRF proxy). This // differs from Play URL, where the URL is handed to the speaker, not fetched - // by soundtouch-web. + // by soundtouch-player. serviceURL := strings.TrimRight(app.ServiceURL, "/") if serviceURL == "" { app.sendError(w, - "TTS requires the AfterTouch service URL. Start soundtouch-web with --service-url .", + "TTS requires the AfterTouch service URL. Start soundtouch-player with --service-url .", http.StatusBadRequest) return diff --git a/pkg/service/soundtouchweb/logutil.go b/pkg/service/soundtouchweb/logutil.go index 730b8f7..dcf84cf 100644 --- a/pkg/service/soundtouchweb/logutil.go +++ b/pkg/service/soundtouchweb/logutil.go @@ -15,7 +15,7 @@ func sanitizeLog(s string) string { return s } -// logPlaybackRequest records what soundtouch-web is about to ask a speaker to +// logPlaybackRequest records what soundtouch-player is about to ask a speaker to // play or switch to. A SoundTouch /select returns HTTP 200 even when the // source is ultimately rejected: the failure only surfaces afterwards as a // now_playing transition to an error source (see logNowPlayingError). So this diff --git a/pkg/service/soundtouchweb/mount.go b/pkg/service/soundtouchweb/mount.go index 222705e..f160440 100644 --- a/pkg/service/soundtouchweb/mount.go +++ b/pkg/service/soundtouchweb/mount.go @@ -10,7 +10,7 @@ import ( "github.com/go-chi/chi/v5" ) -// MountWeb registers the portable soundtouch-web surface on r: the embedded +// MountWeb registers the portable soundtouch-player surface on r: the embedded // assets (/app/static/*), the control API (/api/control/*), and the SPA // (/app/*). It is self-contained under /api/control and /app, registering // nothing outside those subtrees (no /, no /health), so it can be mounted into @@ -128,7 +128,7 @@ func (app *WebApp) MountWeb(r chi.Router, discoveryService *discovery.UnifiedDis r.Get("/app/tts", app.serveIndex) } -// Mount is the standalone soundtouch-web entry point: the portable web surface +// Mount is the standalone soundtouch-player entry point: the portable web surface // (see MountWeb) plus the standalone-only liveness endpoint and a / redirect // into the app. soundtouch-service does not call this β€” it mounts MountWeb and // keeps its own / (landing page) and /health. diff --git a/pkg/service/soundtouchweb/service_client.go b/pkg/service/soundtouchweb/service_client.go index f73724a..459f9c4 100644 --- a/pkg/service/soundtouchweb/service_client.go +++ b/pkg/service/soundtouchweb/service_client.go @@ -11,7 +11,7 @@ import ( // NewServiceHTTPClient builds an *http.Client that trusts the AfterTouch // service's CA certificate (PEM at caPath) in addition to the system trust -// store. soundtouch-web uses it for the only server-side call it makes to the +// store. soundtouch-player uses it for the only server-side call it makes to the // service (the TTS proxy in handlers_tts.go): the service serves a self-signed // certificate signed by its own "AfterTouch Local Root CA", which isn't in any // system trust store, so http.DefaultClient would reject it with diff --git a/pkg/service/soundtouchweb/static/js/components/PlayURL.js b/pkg/service/soundtouchweb/static/js/components/PlayURL.js index d04cdc8..66e859f 100644 --- a/pkg/service/soundtouchweb/static/js/components/PlayURL.js +++ b/pkg/service/soundtouchweb/static/js/components/PlayURL.js @@ -87,7 +87,7 @@ export function PlayURL({ devices, serverServiceUrl }) { /> ${serverServiceUrl - ? html`
Configured server-side (soundtouch-web --service-url); edits here would be ignored.
` + ? html`
Configured server-side (soundtouch-player --service-url); edits here would be ignored.
` : null} ${status && html`
${status}
`} diff --git a/pkg/service/soundtouchweb/static/js/components/TTS.js b/pkg/service/soundtouchweb/static/js/components/TTS.js index 5aa8921..ae707b2 100644 --- a/pkg/service/soundtouchweb/static/js/components/TTS.js +++ b/pkg/service/soundtouchweb/static/js/components/TTS.js @@ -7,10 +7,10 @@ const html = htm.bind(h); // TTS is a "source" view (like PlayURL / TuneIn / RadioBrowser): enter text, // pick a device, and the AfterTouch service synthesizes and plays it. Synthesis, -// credentials, and the service URL all live server-side β€” soundtouch-web proxies +// credentials, and the service URL all live server-side β€” soundtouch-player proxies // to the service it was started with (--service-url). The service URL is shown // read-only: unlike Play URL (whose URL is handed to the speaker), here -// soundtouch-web makes the request itself, so a browser-supplied URL would be an +// soundtouch-player makes the request itself, so a browser-supplied URL would be an // open SSRF proxy. export function TTS({ devices, serverServiceUrl }) { const [text, setText] = useState(''); @@ -56,7 +56,7 @@ export function TTS({ devices, serverServiceUrl }) { type="url" class="tunein-search-input" value=${serverServiceUrl || ''} - placeholder="(not configured β€” start soundtouch-web with --service-url)" + placeholder="(not configured β€” start soundtouch-player with --service-url)" readonly title="AfterTouch service URL β€” set server-side via --service-url" /> @@ -64,9 +64,9 @@ export function TTS({ devices, serverServiceUrl }) {
${serverServiceUrl ? html`Synthesized by the AfterTouch service (Settings β†’ Integrations) and played on the speaker.` - : html`TTS is unavailable: start soundtouch-web with --service-url.`} + : html`TTS is unavailable: start soundtouch-player with --service-url.`}
- The service URL is fixed server-side and can't be edited here: soundtouch-web + The service URL is fixed server-side and can't be edited here: soundtouch-player makes the request itself, so a browser-supplied URL would let anyone use it as an SSRF proxy.
diff --git a/scripts/raspberry-pi/README.md b/scripts/raspberry-pi/README.md index 325c75a..24d3874 100644 --- a/scripts/raspberry-pi/README.md +++ b/scripts/raspberry-pi/README.md @@ -1,7 +1,7 @@ # Raspberry Pi installers Full documentation β€” installation, configuration, service management, updates, -and removal for both `soundtouch-service` and `soundtouch-web` β€” lives in the +and removal for both `soundtouch-service` and `soundtouch-player` β€” lives in the project docs: **[docs/content/docs/guides/RASPBERRY-PI.md](../../docs/content/docs/guides/RASPBERRY-PI.md)** @@ -18,7 +18,7 @@ curl -fsSL -o install.sh \ sudo bash install.sh ``` -**soundtouch-web** (browser control panel): +**soundtouch-player** (browser control panel): ```bash curl -fsSL -o install-web.sh \ diff --git a/scripts/raspberry-pi/install-player.sh b/scripts/raspberry-pi/install-player.sh new file mode 100755 index 0000000..2c62b8a --- /dev/null +++ b/scripts/raspberry-pi/install-player.sh @@ -0,0 +1,338 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================================== +# Bose-SoundTouch soundtouch-player installer (systemd, headless) +# +# Usage: +# sudo bash install-player.sh [vX.Y.Z] +# +# Examples (override defaults via env vars): +# +# sudo \ +# VERSION=v0.107.0 \ +# HTTP_PORT=8081 \ +# bash install-player.sh +# +# # With an AfterTouch service link for TTS (HTTPS + self-signed CA): +# sudo \ +# SERVICE_URL=https://soundtouch.local \ +# SERVICE_CA=/var/lib/soundtouch-service/certs/ca.crt \ +# bash install-player.sh +# +# Or with a version argument to perform an update: +# sudo bash install-player.sh v0.107.0 +# +# Notes: +# - This script downloads a release binary for your CPU (auto-detects armv7/arm64/amd64). +# - soundtouch-player is stateless (no data directory) β€” it is safe to stop/restart freely. +# - Default port is 8080 (unprivileged β€” no special capabilities needed). +# - If soundtouch-service is already installed, soundtouch-player reuses the +# existing soundtouch:soundtouch user/group. +# - Safe to re-run; it will update the binary, env file, and unit and restart. +# ============================================================================== + +VERSION="${1:-${VERSION:-v0.107.0}}" +# Normalize version prefix +if [[ ! "$VERSION" =~ ^v ]]; then + VERSION="v${VERSION}" +fi +SERVICE_NAME="${SERVICE_NAME:-soundtouch-player}" +BIN_PATH="${BIN_PATH:-/usr/local/bin/soundtouch-player}" + +CONFIG_DIR="${CONFIG_DIR:-/etc/soundtouch-player}" +ENV_FILE="${ENV_FILE:-$CONFIG_DIR/soundtouch-player.env}" + +SERVICE_USER="${SERVICE_USER:-soundtouch}" +SERVICE_GROUP="${SERVICE_GROUP:-soundtouch}" + +# Port (unprivileged β€” no CAP_NET_BIND_SERVICE needed) +HTTP_PORT="${HTTP_PORT:-8080}" + +# Optional discovery / device config +BIND_ADDR="${BIND_ADDR:-}" +DISCOVERY_INTERFACE="${DISCOVERY_INTERFACE:-}" +SOUNDTOUCH_DEVICES="${SOUNDTOUCH_DEVICES:-}" + +# Optional AfterTouch service link (needed for TTS / "Speak"). +# SERVICE_URL: base URL of soundtouch-service, e.g. https://soundtouch.local +# SERVICE_CA: path to the service CA cert when it serves HTTPS with its own +# self-signed certificate, e.g. /var/lib/soundtouch-service/certs/ca.crt +SERVICE_URL="${SERVICE_URL:-}" +SERVICE_CA="${SERVICE_CA:-}" + +# Override if you want to force a specific asset suffix: +# ARCH_ASSET=linux-armv7|linux-arm64|linux-amd64 +ARCH_ASSET="${ARCH_ASSET:-}" + +# Internal variables +SCRIPT_PATH="$(realpath "$0" 2>/dev/null || echo "$0")" +IS_SELF_UPDATE="${IS_SELF_UPDATE:-false}" + +log() { printf "\n==> %s\n" "$*"; } +die() { echo "ERROR: $*" >&2; exit 1; } + +need_root() { + [[ "${EUID}" -eq 0 ]] || die "Please run as root (e.g. sudo bash $0)." +} + +ensure_cmd() { + command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1" +} + +apt_install_if_missing() { + log "Installing dependencies: $*" + apt-get update -y + apt-get install -y --no-install-recommends "$@" +} + +detect_arch_asset() { + local m + m="$(uname -m)" + + case "$m" in + armv7l|armv6l) + echo "linux-armv7" + ;; + aarch64) + echo "linux-arm64" + ;; + x86_64|amd64) + echo "linux-amd64" + ;; + *) + die "Unsupported architecture from uname -m: $m (set ARCH_ASSET manually)" + ;; + esac +} + +download_url_for() { + local asset="$1" + echo "https://github.com/gesellix/Bose-SoundTouch/releases/download/${VERSION}/soundtouch-player-${VERSION}-${asset}" +} + +ensure_user_group() { + log "Ensuring service user/group exist: ${SERVICE_USER}:${SERVICE_GROUP}" + if ! getent group "${SERVICE_GROUP}" >/dev/null; then + groupadd --system "${SERVICE_GROUP}" + fi + if ! id -u "${SERVICE_USER}" >/dev/null 2>&1; then + useradd --system \ + --no-create-home \ + --shell /usr/sbin/nologin \ + --gid "${SERVICE_GROUP}" \ + "${SERVICE_USER}" + fi +} + +ensure_dirs() { + log "Creating config directory" + mkdir -p "${CONFIG_DIR}" + chmod 0755 "${CONFIG_DIR}" +} + +download_binary() { + local asset url tmp="" + asset="${ARCH_ASSET:-$(detect_arch_asset)}" + url="$(download_url_for "$asset")" + + log "Downloading binary for ${asset}: ${url}" + tmp="$(mktemp -d)" + trap 'rm -rf "${tmp}"' EXIT + + if command -v curl >/dev/null 2>&1; then + curl -fsSL -o "${tmp}/soundtouch-player" "${url}" + else + wget -qO "${tmp}/soundtouch-player" "${url}" + fi + + chmod +x "${tmp}/soundtouch-player" + + if [[ -f "${BIN_PATH}" ]]; then + log "Backing up existing binary to ${BIN_PATH}.old" + cp -p "${BIN_PATH}" "${BIN_PATH}.old" + fi + + install -m 0755 "${tmp}/soundtouch-player" "${BIN_PATH}" + log "Installed binary to ${BIN_PATH}" +} + +self_update() { + if [[ "$IS_SELF_UPDATE" == "true" ]]; then + return + fi + + local url="https://raw.githubusercontent.com/gesellix/Bose-SoundTouch/${VERSION}/scripts/raspberry-pi/install-player.sh" + local tmp_script="/tmp/soundtouch-player-install-${VERSION}.sh" + + log "Checking for installer updates for ${VERSION}..." + log "URL: ${url}" + + if command -v curl >/dev/null 2>&1; then + if ! curl -fsSL -o "${tmp_script}" "${url}"; then + log "⚠️ Could not fetch installer for ${VERSION}, continuing with current script." + return + fi + else + if ! wget -qO "${tmp_script}" "${url}"; then + log "⚠️ Could not fetch installer for ${VERSION}, continuing with current script." + return + fi + fi + + if diff -q "${SCRIPT_PATH}" "${tmp_script}" >/dev/null 2>&1; then + log "Installer is already up to date." + rm -f "${tmp_script}" + return + fi + + log "Newer installer found for ${VERSION}. Updating ${SCRIPT_PATH} and re-executing..." + install -m 0755 "${tmp_script}" "${SCRIPT_PATH}" + rm -f "${tmp_script}" + + export IS_SELF_UPDATE="true" + export VERSION HTTP_PORT BIND_ADDR DISCOVERY_INTERFACE SOUNDTOUCH_DEVICES + export SERVICE_URL SERVICE_CA + export BIN_PATH CONFIG_DIR ENV_FILE SERVICE_USER SERVICE_GROUP + + exec "${SCRIPT_PATH}" "$@" +} + +write_env_file() { + log "Updating env file: ${ENV_FILE}" + + local vars=( + "PORT=${HTTP_PORT}" + "BIND_ADDR=${BIND_ADDR}" + "DISCOVERY_INTERFACE=${DISCOVERY_INTERFACE}" + "SOUNDTOUCH_DEVICES=${SOUNDTOUCH_DEVICES}" + "SERVICE_URL=${SERVICE_URL}" + "SERVICE_CA=${SERVICE_CA}" + ) + + if [[ ! -f "${ENV_FILE}" ]]; then + for entry in "${vars[@]}"; do + echo "${entry}" >> "${ENV_FILE}" + done + else + for entry in "${vars[@]}"; do + local key="${entry%%=*}" + local val="${entry#*=}" + if ! grep -q "^${key}=" "${ENV_FILE}"; then + echo "${key}=${val}" >> "${ENV_FILE}" + fi + done + fi + + chmod 0640 "${ENV_FILE}" + chown root:"${SERVICE_GROUP}" "${ENV_FILE}" || true +} + +write_systemd_unit() { + log "Writing systemd unit: /etc/systemd/system/${SERVICE_NAME}.service" + cat > "/etc/systemd/system/${SERVICE_NAME}.service" </dev/null 2>&1; then + success=true + break + fi + echo "Waiting for service to respond at $health_url... ($((count+1))/$max_retries)" + sleep 2 + count=$((count+1)) + done + + if [[ "$success" = true ]]; then + log "βœ… soundtouch-player is healthy and responding!" + else + log "⚠️ Service started but did not respond at $health_url within timeout." + log "Check logs with: journalctl -u ${SERVICE_NAME}.service -n 50" + fi +} + +show_status() { + log "Service status" + systemctl --no-pager --full status "${SERVICE_NAME}.service" || true + + log "Listening socket (:${HTTP_PORT})" + ss -tulpn | grep -E ":${HTTP_PORT}\b" || true + + if command -v ufw >/dev/null 2>&1 && ufw status | grep -q "Status: active"; then + log "Firewall check (UFW is active)" + if ! ufw status | grep -qE "${HTTP_PORT}.*ALLOW"; then + log "⚠️ UFW is active but port ${HTTP_PORT} might be blocked." + log "Run: sudo ufw allow ${HTTP_PORT}/tcp" + else + log "βœ… UFW rule for port ${HTTP_PORT} appears to be in place." + fi + fi + + cat <:${HTTP_PORT}/ + +soundtouch-player is a control panel β€” you can stop it when not in use: + sudo systemctl stop ${SERVICE_NAME} + sudo systemctl start ${SERVICE_NAME} + +Logs: + journalctl -u ${SERVICE_NAME}.service -e --no-pager +EOF +} + +main() { + need_root + ensure_cmd systemctl + ensure_cmd ss + + if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then + apt_install_if_missing curl + fi + + self_update "$@" + + ensure_user_group + ensure_dirs + download_binary + write_env_file + write_systemd_unit + reload_enable_start + show_status +} + +main "$@" diff --git a/scripts/raspberry-pi/install-web.sh b/scripts/raspberry-pi/install-web.sh index e8aa109..47d6761 100644 --- a/scripts/raspberry-pi/install-web.sh +++ b/scripts/raspberry-pi/install-web.sh @@ -1,6 +1,17 @@ #!/usr/bin/env bash set -euo pipefail +# ============================================================================== +# DEPRECATED: soundtouch-web has been renamed to soundtouch-player. +# This installer still works (the soundtouch-web release asset is published as +# a transitional alias) but will be removed in a future release. Please switch +# to install-player.sh, which installs the soundtouch-player binary/service. +# ============================================================================== +echo "NOTICE: soundtouch-web has been renamed to soundtouch-player." >&2 +echo " This installer (install-web.sh) is deprecated and will be removed" >&2 +echo " in a future release; please switch to install-player.sh." >&2 +echo >&2 + # ============================================================================== # Bose-SoundTouch soundtouch-web installer (systemd, headless) #