From adcdc26d8d06e79afa52300d2255a8b4ce08f1a9 Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Thu, 28 May 2026 22:52:55 +0200 Subject: [PATCH] feat(web): add RPi installer for soundtouch-web + GET /health endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add scripts/raspberry-pi/install-web.sh: mirrors install.sh but for the stateless soundtouch-web binary (no privileged ports, no data dir, no HTTPS). Default port 8080; override via HTTP_PORT at install time. - Add GET /health to soundtouch-web (handler + mount); returns {"status":"ok","version":"…"} — used by the installer's health check and by monitoring. - Update scripts/raspberry-pi/README.md to document both installers side by side (installation, config, service management, updates, removal). - Bump default VERSION to v0.97.0 in all three installer scripts (install.sh, install-web.sh, on-device-install/install.sh). Co-Authored-By: Claude Sonnet 4.6 --- pkg/service/soundtouchweb/handler.go | 9 + pkg/service/soundtouchweb/mount.go | 3 + scripts/on-device-install/install.sh | 4 +- scripts/raspberry-pi/README.md | 326 +++++++++++---------------- scripts/raspberry-pi/install-web.sh | 322 ++++++++++++++++++++++++++ scripts/raspberry-pi/install.sh | 8 +- 6 files changed, 474 insertions(+), 198 deletions(-) create mode 100644 scripts/raspberry-pi/install-web.sh diff --git a/pkg/service/soundtouchweb/handler.go b/pkg/service/soundtouchweb/handler.go index eba1fb6..ef7b758 100644 --- a/pkg/service/soundtouchweb/handler.go +++ b/pkg/service/soundtouchweb/handler.go @@ -1101,6 +1101,15 @@ func (app *WebApp) HandleAPIVersion(w http.ResponseWriter, _ *http.Request) { } } +// HandleHealth returns a minimal liveness response. +func (app *WebApp) HandleHealth(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + + if err := json.NewEncoder(w).Encode(map[string]string{"status": "ok", "version": app.Version}); err != nil { + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + } +} + // HandleRadioBrowserSearch handles RadioBrowser search requests. func (app *WebApp) HandleRadioBrowserSearch(w http.ResponseWriter, r *http.Request) { query := r.URL.Query().Get("q") diff --git a/pkg/service/soundtouchweb/mount.go b/pkg/service/soundtouchweb/mount.go index 2b33e4d..00ba657 100644 --- a/pkg/service/soundtouchweb/mount.go +++ b/pkg/service/soundtouchweb/mount.go @@ -22,6 +22,9 @@ func (app *WebApp) Mount(r chi.Router, discoveryService *discovery.UnifiedDiscov // WebSocket endpoint r.Get("/ws", app.HandleWebSocket) + // Health / liveness + r.Get("/health", app.HandleHealth) + // API endpoints r.Get("/api/devices", app.HandleAPIDevices) r.Get("/api/device/{id}", app.HandleAPIDevice) diff --git a/scripts/on-device-install/install.sh b/scripts/on-device-install/install.sh index b583711..4d48dd9 100644 --- a/scripts/on-device-install/install.sh +++ b/scripts/on-device-install/install.sh @@ -8,8 +8,8 @@ set -eo pipefail # # Override via environment variable or the --version/-v flag: # VERSION=0.92.0 curl -sSL .../install.sh | sh -# curl -sSL .../install.sh | sh -s -- --version 0.92.0 -VERSION=${VERSION:-0.93.1} +# curl -sSL .../install.sh | sh -s -- --version 0.97.0 +VERSION=${VERSION:-0.97.0} # Parse optional command-line arguments so the script can be invoked as: # install.sh --version 0.92.0 diff --git a/scripts/raspberry-pi/README.md b/scripts/raspberry-pi/README.md index 1dbf4e5..f3886d2 100644 --- a/scripts/raspberry-pi/README.md +++ b/scripts/raspberry-pi/README.md @@ -1,45 +1,39 @@ -Here is a `README.md` you can place next to your install script (or in your repo) to document installation, configuration, updates, and debugging. +# Raspberry Pi installers + +Two installer scripts are available, one for each binary: + +| Script | Binary | Role | Default port | +|------------------|----------------------|-----------------------------------------|--------------| +| `install.sh` | `soundtouch-service` | Cloud-replacement relay — must run 24/7 | 80 / 443 | +| `install-web.sh` | `soundtouch-web` | Browser control panel — run on demand | 8080 | + +Both scripts auto-detect CPU architecture (armv7 / arm64 / amd64), create a systemd unit, +and are safe to re-run for updates. --- -# SoundTouch Service (systemd install) +# soundtouch-service -This setup installs `soundtouch-service` from the official GitHub release and runs it as a hardened systemd service. - -It supports: - -* Automatic start on boot -* Binding to privileged ports (80 / 443) without running as root -* Config via environment file -* Clean updates -* Safe re-runs of the installer - ---- - -# Installation - -Run the installer script: +## Installation ```bash -sudo bash install-soundtouch-service.sh +curl -fsSL -o install.sh \ + https://raw.githubusercontent.com/gesellix/Bose-SoundTouch/main/scripts/raspberry-pi/install.sh +sudo bash install.sh ``` -You can override defaults: +Override defaults at install time: ```bash sudo \ - VERSION=v0.93.1 \ + VERSION=v0.97.0 \ HOSTNAME_FQDN=soundtouch.local \ HTTP_PORT=80 \ HTTPS_PORT=443 \ - bash install-soundtouch-service.sh + bash install.sh ``` ---- - -# Configuration - -Configuration lives in: +## Configuration ``` /etc/soundtouch-service/soundtouch-service.env @@ -61,139 +55,41 @@ SERVER_URL=http://soundtouch.local HTTPS_SERVER_URL=https://soundtouch.local ``` ---- - -# Important: Applying Configuration Changes - -If you change the environment file, you must reload and restart the service. - -Full roundtrip: - -```bash -sudo systemctl daemon-reload -sudo systemctl restart soundtouch-service -``` - -Usually `daemon-reload` is only needed if the **unit file** changed. - -If only the `.env` file changed: +After editing the env file: ```bash sudo systemctl restart soundtouch-service ``` ---- - -# Service Management - -Check status: +## Service management ```bash systemctl status soundtouch-service -``` - -Enable at boot: - -```bash -sudo systemctl enable soundtouch-service -``` - -Disable: - -```bash +sudo systemctl enable soundtouch-service # start on boot sudo systemctl disable soundtouch-service -``` - -Stop / start manually: - -```bash sudo systemctl stop soundtouch-service sudo systemctl start soundtouch-service +sudo systemctl restart soundtouch-service ``` ---- - -# Logs & Debugging - -View recent logs: +## Logs ```bash -journalctl -u soundtouch-service -e --no-pager +journalctl -u soundtouch-service -e --no-pager # recent +journalctl -u soundtouch-service -f # follow +journalctl -u soundtouch-service -b # this boot ``` -Follow logs live: - -```bash -journalctl -u soundtouch-service -f -``` - -Show logs from current boot: - -```bash -journalctl -u soundtouch-service -b -``` - -If the service fails to start: - -```bash -systemctl status soundtouch-service --no-pager -``` - -Look for: - -* `bind: permission denied` → capability issue -* `address already in use` → port conflict -* permission errors in DATA_DIR → ownership issue - ---- - -# Port Conflicts - -Check if 80/443 are in use: - -```bash -sudo ss -tulpn | grep -E ':80|:443' -``` - -If another service is using the port, either: - -* stop/disable that service -* or change `PORT` / `HTTPS_PORT` in the env file - -Then restart the service. - ---- - -# Updating to a New Version - -To upgrade, simply run the installer with the desired version as an argument: +## Updates ```bash sudo bash install.sh vX.Y.Z ``` -The script will: +The script self-updates, downloads the new binary, backs up the old one to `.old`, and +restarts the service. Your env file and data are preserved. -* Automatically fetch the latest version of the installer script for that release -* Download the new service binary -* Backup the old binary to `.old` -* Overwrite the binary and restart the service - -No need to reconfigure anything; your existing `.env` file and data will be preserved. - ---- - -# Reinstall / Reset - -To fully reset: - -```bash -sudo systemctl stop soundtouch-service -sudo rm -rf /var/lib/soundtouch-service/* -sudo systemctl start soundtouch-service -``` - -To completely remove: +## Removal ```bash sudo systemctl disable --now soundtouch-service @@ -206,72 +102,118 @@ sudo systemctl daemon-reload --- -# Architecture Auto-Detection +# soundtouch-web -The installer auto-detects: - -* `linux-armv7` -* `linux-arm64` -* `linux-amd64` - -Override manually if needed: +## Installation ```bash -sudo ARCH_ASSET=linux-arm64 bash install-soundtouch-service.sh +curl -fsSL -o install-web.sh \ + https://raw.githubusercontent.com/gesellix/Bose-SoundTouch/main/scripts/raspberry-pi/install-web.sh +sudo bash install-web.sh +``` + +Override defaults at install time: + +```bash +sudo \ + VERSION=v0.97.0 \ + HTTP_PORT=8081 \ + bash install-web.sh +``` + +`soundtouch-web` is **stateless** — it holds no persistent data and can be stopped or +restarted at any time without data loss. + +## Configuration + +``` +/etc/soundtouch-web/soundtouch-web.env +``` + +Example: + +```bash +PORT=8080 +BIND_ADDR= +DISCOVERY_INTERFACE= +SOUNDTOUCH_DEVICES= +``` + +`SOUNDTOUCH_DEVICES` accepts a comma-separated list of IP addresses for manual device +registration (useful when mDNS auto-discovery is unreliable on your network). + +After editing the env file: + +```bash +sudo systemctl restart soundtouch-web +``` + +## Port conflicts + +Port 8080 is a common default for other services. To use a different port, either pass +`HTTP_PORT=` to the installer, or edit the env file after installation: + +```bash +sudo ss -tulpn | grep :8080 # check what's using the port +``` + +## 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 +``` + +## Logs + +```bash +journalctl -u soundtouch-web -e --no-pager +journalctl -u soundtouch-web -f +``` + +## Updates + +```bash +sudo bash install-web.sh vX.Y.Z +``` + +## 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 daemon-reload ``` --- -# Security Notes +# Architecture auto-detection -The service: +Both installers detect the CPU and pick the matching release asset automatically: -* Runs as a dedicated `soundtouch` system user -* Uses `AmbientCapabilities=CAP_NET_BIND_SERVICE` -* Does not require `setcap` -* Does not run as root -* Uses systemd sandboxing (`ProtectSystem`, `PrivateTmp`, etc.) +| `uname -m` | asset suffix | +|---------------------|---------------| +| `aarch64` | `linux-arm64` | +| `armv7l` / `armv6l` | `linux-armv7` | +| `x86_64` | `linux-amd64` | + +Override if needed: + +```bash +sudo ARCH_ASSET=linux-arm64 bash install.sh +sudo ARCH_ASSET=linux-arm64 bash install-web.sh +``` --- -# Quick Troubleshooting Checklist +# Security -If something does not work: - -1. Check status: - - ``` - systemctl status soundtouch-service - ``` - -2. Check logs: - - ``` - journalctl -u soundtouch-service -e - ``` - -3. Confirm ports: - - ``` - ss -tulpn | grep -E ':80|:443' - ``` - -4. Confirm env file: - - ``` - cat /etc/soundtouch-service/soundtouch-service.env - ``` - -5. Restart cleanly: - - ``` - sudo systemctl restart soundtouch-service - ``` - ---- - -If you’d like, I can also provide: - -* A `make update` style wrapper -* A rollback mechanism -* Or a self-update script with checksum verification +Both services run as the `soundtouch` system user (no login shell, no home directory +ownership required for `soundtouch-web`). `soundtouch-service` additionally uses +`AmbientCapabilities=CAP_NET_BIND_SERVICE` to bind ports 80/443 without root. diff --git a/scripts/raspberry-pi/install-web.sh b/scripts/raspberry-pi/install-web.sh new file mode 100644 index 0000000..eef2e29 --- /dev/null +++ b/scripts/raspberry-pi/install-web.sh @@ -0,0 +1,322 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================================== +# Bose-SoundTouch soundtouch-web installer (systemd, headless) +# +# Usage: +# sudo bash install-web.sh [vX.Y.Z] +# +# Examples (override defaults via env vars): +# +# sudo \ +# VERSION=v0.95.0 \ +# HTTP_PORT=8081 \ +# bash install-web.sh +# +# Or with a version argument to perform an update: +# sudo bash install-web.sh v0.95.0 +# +# Notes: +# - This script downloads a release binary for your CPU (auto-detects armv7/arm64/amd64). +# - soundtouch-web 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-web 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.97.0}}" +# Normalize version prefix +if [[ ! "$VERSION" =~ ^v ]]; then + VERSION="v${VERSION}" +fi +SERVICE_NAME="${SERVICE_NAME:-soundtouch-web}" +BIN_PATH="${BIN_PATH:-/usr/local/bin/soundtouch-web}" + +CONFIG_DIR="${CONFIG_DIR:-/etc/soundtouch-web}" +ENV_FILE="${ENV_FILE:-$CONFIG_DIR/soundtouch-web.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:-}" + +# 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-web-${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-web" "${url}" + else + wget -qO "${tmp}/soundtouch-web" "${url}" + fi + + chmod +x "${tmp}/soundtouch-web" + + 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-web" "${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-web.sh" + local tmp_script="/tmp/soundtouch-web-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 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}" + ) + + 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-web 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-web 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.sh b/scripts/raspberry-pi/install.sh index 347f7a6..dd4534b 100644 --- a/scripts/raspberry-pi/install.sh +++ b/scripts/raspberry-pi/install.sh @@ -10,7 +10,7 @@ set -euo pipefail # Examples (override defaults via env vars): # # sudo \ -# VERSION=v0.93.1 \ +# VERSION=v0.97.0 \ # HOSTNAME_FQDN=soundtouch.local \ # HTTP_PORT=80 \ # HTTPS_PORT=443 \ @@ -18,7 +18,7 @@ set -euo pipefail # bash install.sh # # Or with a version argument to perform an update: -# sudo bash install.sh v0.93.1 +# sudo bash install.sh v0.97.0 # # Notes: # - This script downloads a release binary for your CPU (auto-detects armv7/arm64/amd64). @@ -28,7 +28,7 @@ set -euo pipefail # - Safe to re-run; it will update binary/config/unit and restart the service. # ============================================================================== -VERSION="${1:-${VERSION:-v0.93.1}}" +VERSION="${1:-${VERSION:-v0.97.0}}" # Normalize version prefix if [[ ! "$VERSION" =~ ^v ]]; then VERSION="v${VERSION}" @@ -117,7 +117,7 @@ detect_arch_asset() { download_url_for() { local asset="$1" # Release asset pattern used by you earlier: - # soundtouch-service-v0.93.1-linux-armv7 + # soundtouch-service-v0.97.0-linux-armv7 echo "https://github.com/gesellix/Bose-SoundTouch/releases/download/${VERSION}/soundtouch-service-${VERSION}-${asset}" }