Prepare Spotify primer

This commit is contained in:
Tobias Gesellchen
2026-02-21 23:40:45 +01:00
parent c37e94b5f8
commit 8642ecfc5c
16 changed files with 1628 additions and 69 deletions
+97
View File
@@ -0,0 +1,97 @@
# On-Speaker Spotify Boot Primer for Bose SoundTouch
Self-contained boot-time Spotify primer that runs directly on the speaker.
No Spotify credentials on the device — it fetches a fresh token from a
[Bose-SoundTouch](https://github.com/gesellix/Bose-SoundTouch) server at boot.
No jq, no rootfs modification — just files on persistent storage.
## How It Works
Bose SoundTouch speakers run embedded Linux with a persistent writable volume
at `/mnt/nv`. The init script `shelby_local` (S97) has a built-in hook:
```
[ -x /mnt/nv/rc.local ] && /mnt/nv/rc.local
```
This runs before SoundTouch itself (S99), so we background a primer script
that waits for the Spotify Connect ZeroConf endpoint (port 8200) to come up,
fetches a fresh Spotify token from the service, and primes the speaker — all
within ~30 seconds of boot.
## File Layout
```
/mnt/nv/
rc.local boot hook (S97 checks this)
.profile PATH setup for interactive SSH
bin/
spotify-boot-primer main script
BoseApp-Persistence/1/
spotify-primer.conf service credentials (mode 600)
Sources.xml, Presets.xml, ... existing speaker data
```
Scripts live in `/mnt/nv/bin/` (added to PATH via `.profile`), config lives
alongside the speaker's own persistence files in `/mnt/nv/BoseApp-Persistence/1/`.
## Speaker Environment
Tested on SoundTouch 20. Other SoundTouch models likely similar.
| Item | Detail |
|------|--------|
| OS | Linux 3.14.43+ ARM (hostname `spotty`) |
| Root FS | Read-only ubifs (can be remounted rw) |
| Persistent storage | `/mnt/nv` — writable ubifs, ~24M free |
| curl | 7.50.3 with OpenSSL (HTTPS works) |
| bash/grep/sed/awk | Available via busybox |
| jq | **Not available** (not needed) |
| Init | SysV, runlevel 5 |
| Production mode | Yes — cron is disabled |
## Prerequisites
1. **SSH access to the speaker**:
```
ssh -o HostKeyAlgorithms=+ssh-rsa -o PubkeyAcceptedAlgorithms=+ssh-rsa root@SPEAKER_IP
```
2. **A running [Bose-SoundTouch](https://github.com/gesellix/Bose-SoundTouch) server** with:
- A linked Spotify account (via the management API OAuth flow)
- The `GET /mgmt/spotify/token` endpoint (returns `{accessToken, username}`)
- Management API credentials (HTTP Basic Auth)
## Installation
SSH into the speaker and run:
```bash
# 1. Create bin directory
mkdir -p /mnt/nv/bin
# 2. Create the config file with your service connection info
cat > /mnt/nv/BoseApp-Persistence/1/spotify-primer.conf << 'EOF'
SOUNDTOUCH_URL=https://soundtouch.example.com
SOUNDTOUCH_USER=admin
SOUNDTOUCH_PASS=secret
EOF
chmod 600 /mnt/nv/BoseApp-Persistence/1/spotify-primer.conf
# 3. Copy spotify-boot-primer to the speaker
# From your local machine:
# cat scripts/spotify/spotify-boot-primer | ssh root@SPEAKER_IP "cat > /mnt/nv/bin/spotify-boot-primer"
chmod +x /mnt/nv/bin/spotify-boot-primer
# 4. Create the boot hook
cat > /mnt/nv/rc.local << 'EOF'
#!/bin/bash
/mnt/nv/bin/spotify-boot-primer &
EOF
chmod +x /mnt/nv/rc.local
# 5. Set up PATH for interactive SSH sessions (optional but convenient)
cat > /mnt/nv/.profile << 'EOF'
export PATH="/mnt/nv/bin:$PATH"
EOF
```
## Testing
```bash
# Manual test (speaker must be running):
/mnt/nv/bin/spotify-boot-primer
# Check logs:
logread | grep spotify-primer
# Full test — reboot the speaker:
reboot
# Wait ~30s, then SSH back in and check:
logread | grep spotify-primer
curl -s "http://localhost:8200/zc?action=getInfo" | grep activeUser
```
## Related
- [Bose-SoundTouch](https://github.com/gesellix/Bose-SoundTouch) — Comprehensive Go toolkit with migration automation
+6
View File
@@ -0,0 +1,6 @@
# Spotify Scripts
This directory contains scripts and configuration files for the Spotify OAuth integration, specifically for priming Bose SoundTouch speakers.
These files were adapted from the community gist:
https://gist.github.com/timvw/84ef8768ff876ef6805012b3eb4015b0
+318
View File
@@ -0,0 +1,318 @@
# ZeroConf Analysis - Spotify Connect Integration for Bose SoundTouch
## Overview
This document provides a comprehensive analysis of the Spotify Connect ZeroConf protocol as implemented by Bose SoundTouch speakers. ZeroConf enables seamless integration between Spotify clients and SoundTouch hardware without requiring manual configuration.
## What is ZeroConf in This Context?
ZeroConf (Zero Configuration) in the Bose SoundTouch ecosystem is a **Spotify Connect integration protocol** that allows Spotify clients (mobile apps, desktop applications) to discover and control SoundTouch speakers automatically. The speakers expose an HTTP API on **port 8200** that implements Spotify's official ZeroConf specification.
## Network Discovery
### mDNS/Bonjour Advertisement
SoundTouch speakers advertise themselves on the local network using:
- **Service Type**: `_spotify-connect._tcp`
- **Port**: 8200
- **TXT Record**: `CPath=/zc` (points to the ZeroConf endpoint)
This allows Spotify applications to automatically discover available speakers without manual configuration.
### Endpoint Structure
```
http://[SPEAKER_IP]:8200/zc?action=[ACTION]&[PARAMETERS]
```
Example: `http://192.168.1.100:8200/zc?action=getInfo`
## The getInfo Action
### Purpose
The `getInfo` action retrieves comprehensive device information and current status. This is the most commonly used ZeroConf action for:
- Device discovery and identification
- Checking Spotify authentication status
- Retrieving device capabilities
- Monitoring multiroom configurations
### Request Format
```http
GET http://[SPEAKER_IP]:8200/zc?action=getInfo&version=2.10.0
```
The `version` parameter is optional but recommended for compatibility.
### Response Properties
#### Mandatory Fields (Present in All Responses)
| Property | Type | Description |
|----------|------|-------------|
| `status` | Integer | Operation result code (101 = success) |
| `statusString` | String | Human-readable status description |
| `spotifyError` | Integer | Last Spotify SDK error code (0 = no error) |
| `responseSource` | String | Entity identifier (e.g., "Bose") |
#### Device Information Fields
| Property | Required | Type | Description |
|----------|----------|------|-------------|
| `version` | Yes | String | ZeroConf API version (e.g., "2.10.0") |
| `deviceID` | Yes | String | Unique device identifier (MAC-based) |
| `publicKey` | Yes | String | Device's public key for secure communication |
| `remoteName` | Yes | String | User-friendly device name shown in Spotify |
| `deviceType` | No | String | Device category (e.g., "SPEAKER") |
| `brandDisplayName` | Yes | String | Brand name displayed in Spotify apps |
| `modelDisplayName` | No | String | Model name for user display |
| `libraryVersion` | Yes | String | Spotify Connect library version |
| `resolverVersion` | Yes | String | DNS resolution version |
| `groupStatus` | Yes | String | Multiroom status: "NONE", "GROUP", or "SLAVE" |
| `tokenType` | Yes | String | Authentication token type ("accesstoken") |
| `clientID` | Yes | String | Spotify client identifier |
| `productID` | Yes | Integer | Spotify product identifier |
| `scope` | Yes | String | Permission scope (typically "streaming") |
| `availability` | Yes | String | Device availability status |
#### Status Fields
| Property | Required | Type | Description |
|----------|----------|------|-------------|
| `activeUser` | No | String | Currently logged-in Spotify username (if any) |
#### Advanced Fields (Optional)
| Property | Type | Description |
|----------|------|-------------|
| `aliases` | Array | Virtual devices for multiroom zones |
| `supported_drm_media_formats` | Array | Supported audio formats with DRM capabilities |
| `supported_capabilities` | Integer | Bitmasked device capabilities |
### Example Response
```json
{
"status": 101,
"statusString": "OK",
"spotifyError": 0,
"responseSource": "Bose",
"version": "2.10.0",
"deviceID": "0007F537F5ED",
"deviceType": "SPEAKER",
"remoteName": "Living Room Speaker",
"publicKey": "BgIwVfz9ZXQG...",
"brandDisplayName": "Bose",
"modelDisplayName": "SoundTouch 30",
"libraryVersion": "master-v3.15.1-g7890abcd",
"resolverVersion": "1",
"groupStatus": "NONE",
"tokenType": "accesstoken",
"clientID": "65b708073fc0480ea92a077233ca87bd",
"productID": 0,
"scope": "streaming",
"availability": "",
"activeUser": "spotify_username",
"supported_drm_media_formats": [
{"drm": 0, "formats": 35},
{"drm": 1, "formats": 35},
{"drm": 3, "formats": 1168}
],
"supported_capabilities": 1
}
```
## Key Properties Analysis
### Critical Status Indicators
- **`activeUser`**: Most important field for determining if Spotify is active
- Present and non-empty: Spotify is authenticated and ready
- Empty or missing: No active Spotify session
- **`remoteName`**: The display name users see in Spotify Connect device lists
- Should be descriptive and user-friendly
- Can contain UTF-8 characters and special symbols
### Device Identification
- **`deviceID`**: Unique identifier for targeting specific speakers
- Typically derived from MAC address
- Used for device-specific API calls
- **`groupStatus`**: Critical for multiroom functionality
- `"NONE"`: Standalone device
- `"GROUP"`: Multiroom master/coordinator
- `"SLAVE"`: Member of a multiroom group
### Display Properties
- **`brandDisplayName`** and **`modelDisplayName`**: Shown in Spotify client UIs
- Should be marketing-appropriate names
- Support UTF-8 for international markets
## Practical Usage Examples
### 1. Status Checking
```bash
# Check if Spotify is active
curl -s "http://192.168.1.100:8200/zc?action=getInfo" | \
grep -o '"activeUser" *: *"[^"]*"' | \
sed 's/"activeUser" *: *"//;s/"$//'
```
### 2. Device Discovery
```bash
# Get device name and ID
info=$(curl -s "http://192.168.1.100:8200/zc?action=getInfo")
device_name=$(echo "$info" | grep -o '"remoteName" *: *"[^"]*"' | sed 's/"remoteName" *: *"//;s/"$//')
device_id=$(echo "$info" | grep -o '"deviceID" *: *"[^"]*"' | sed 's/"deviceID" *: *"//;s/"$//')
```
### 3. Multiroom Detection
```bash
# Check multiroom status
group_status=$(curl -s "http://192.168.1.100:8200/zc?action=getInfo" | \
grep -o '"groupStatus" *: *"[^"]*"' | \
sed 's/"groupStatus" *: *"//;s/"$//')
```
## Authentication Flow
The ZeroConf API supports the `addUser` action for Spotify authentication:
```bash
curl -X POST "http://192.168.1.100:8200/zc" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "action=addUser&userName=${SPOTIFY_USER}&blob=${ACCESS_TOKEN}&clientKey=&tokenType=accesstoken"
```
### Token Requirements
- **Access Token**: Valid Spotify OAuth access token
- **Username**: Spotify username associated with the token
- **Token Type**: Always "accesstoken" for current implementations
- **Client Key**: Empty string for current protocol version
### Token Lifecycle
1. Tokens expire after 1 hour (3600 seconds)
2. Speakers must be re-primed after reboot
3. Use `getInfo` to verify successful authentication via `activeUser` field
## Security Considerations
### Communication Security
- **Protocol**: HTTP (plain text) is standard, HTTPS supported but optional
- **Network Scope**: Local network only (port 8200 typically not exposed externally)
- **Authentication**: Token-based, no permanent credentials stored
### Best Practices
1. **Token Management**:
- Never store long-lived tokens on devices
- Implement token refresh mechanisms
- Use centralized token servers when possible
2. **Network Security**:
- Ensure port 8200 is not accessible from external networks
- Consider HTTPS for enhanced security
- Implement proper firewall rules
3. **Error Handling**:
- Always check `status` and `spotifyError` fields
- Implement retry mechanisms for network failures
- Log authentication failures for debugging
## Integration Patterns
### Boot-time Automation
See `spotify-boot-primer.sh` for a complete example of:
1. Waiting for ZeroConf endpoint availability
2. Checking current authentication status
3. Fetching fresh tokens from a management server
4. Automatically priming speakers at startup
### Manual Priming
See `spotify-prime-speaker.sh` for standalone token injection:
1. Validate access tokens against Spotify API
2. Extract username from token metadata
3. Prime individual speakers
4. Verify successful authentication
### Monitoring and Health Checks
```bash
#!/bin/bash
# Health check script
SPEAKER_IP="192.168.1.100"
info=$(curl -sf --max-time 5 "http://${SPEAKER_IP}:8200/zc?action=getInfo" 2>/dev/null)
if [ $? -eq 0 ]; then
active_user=$(echo "$info" | grep -o '"activeUser" *: *"[^"]*"' | sed 's/"activeUser" *: *"//;s/"$//')
if [ -n "$active_user" ]; then
echo "✅ Spotify active (user: $active_user)"
else
echo "⚠️ Speaker reachable but Spotify not active"
fi
else
echo "❌ Speaker unreachable"
fi
```
## Troubleshooting
### Common Issues
1. **Port 8200 Unreachable**
- Check network connectivity
- Verify speaker is powered on
- Confirm IP address is correct
2. **Empty `activeUser` After Authentication**
- Wait 2-5 seconds after `addUser` request
- Verify access token is valid and not expired
- Check `spotifyError` field for SDK errors
3. **Authentication Failures**
- Ensure token has correct scopes
- Verify username matches token owner
- Check token expiration time
### Diagnostic Commands
```bash
# Test basic connectivity
curl -sf --max-time 5 "http://192.168.1.100:8200/zc?action=getInfo"
# Check detailed response
curl -s "http://192.168.1.100:8200/zc?action=getInfo" | jq .
# Monitor authentication status
while true; do
active=$(curl -s "http://192.168.1.100:8200/zc?action=getInfo" | \
grep -o '"activeUser" *: *"[^"]*"' | sed 's/"activeUser" *: *"//;s/"$//')
echo "$(date): activeUser = '$active'"
sleep 10
done
```
## References
- [Spotify ZeroConf API Documentation](https://developer.spotify.com/documentation/commercial-hardware/implementation/guides/zeroconf)
- [Bose SoundTouch Toolkit](https://github.com/gesellix/Bose-SoundTouch)
- Scripts in this directory:
- `spotify-boot-primer.sh`: Automated boot-time priming
- `spotify-prime-speaker.sh`: Manual speaker priming
- `spotify-primer.conf.example`: Configuration template
---
*This analysis is based on Spotify's official ZeroConf specification and practical implementation experience with Bose SoundTouch speakers.*
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
# /mnt/nv/rc.local — runs at boot via shelby_local (S97)
# Launches Spotify boot primer in background since SoundTouch starts at S99
/mnt/nv/bin/spotify-boot-primer &
+144
View File
@@ -0,0 +1,144 @@
#!/bin/bash
#
# spotify-boot-primer — Self-contained Spotify primer for Bose SoundTouch speakers
#
# Runs at boot (via /mnt/nv/rc.local), waits for the ZeroConf endpoint to
# come up, fetches a fresh Spotify token from a soundtouch-service server, and
# primes the speaker. No Spotify credentials stored on the device.
#
# Only needs: curl, grep, sed (all available on the speaker via busybox).
#
# Install:
# 1. mkdir -p /mnt/nv/soundtouch-service
# 2. Copy this script to /mnt/nv/soundtouch-service/spotify-boot-primer
# 3. Create /mnt/nv/soundtouch-service/spotify-primer.conf
# 4. Create /mnt/nv/rc.local that backgrounds this script
# 5. chmod +x /mnt/nv/rc.local /mnt/nv/soundtouch-service/spotify-boot-primer
#
# Config file format (/mnt/nv/soundtouch-service/spotify-primer.conf):
# SOUNDTOUCH_URL=https://soundtouch.example.com
# SOUNDTOUCH_USER=admin
# SOUNDTOUCH_PASS=secret
#
# Related:
# https://github.com/gesellix/Bose-SoundTouch
#
set -uo pipefail
CONF="/mnt/nv/soundtouch-service/spotify-primer.conf"
LOG_TAG="spotify-primer[$$]"
ZC_URL="http://localhost:8200/zc"
MAX_WAIT=120 # max seconds to wait for port 8200
RETRY_DELAY=3 # seconds between retries
# --- Logging ---
log() {
logger -s -t "$LOG_TAG" -p "$1" "$2"
}
# --- JSON parsing without jq ---
# Extract a string value: echo '{"key":"val"}' | json_str key
json_str() {
grep -o "\"$1\" *: *\"[^\"]*\"" | sed "s/\"$1\" *: *\"//;s/\"$//"
}
# Extract a numeric value: echo '{"key":123}' | json_num key
json_num() {
grep -o "\"$1\" *: *[0-9]*" | sed "s/\"$1\" *: *//"
}
# --- Load config ---
if [ ! -f "$CONF" ]; then
log err "Config not found: $CONF"
exit 1
fi
. "$CONF"
for var in SOUNDTOUCH_URL SOUNDTOUCH_USER SOUNDTOUCH_PASS; do
if [ -z "${!var:-}" ]; then
log err "Missing $var in $CONF"
exit 1
fi
done
log info "Config loaded (server=${SOUNDTOUCH_URL})"
# --- Wait for ZeroConf endpoint (port 8200) ---
log info "Waiting for ZeroConf endpoint (max ${MAX_WAIT}s)..."
waited=0
while true; do
if curl -sf --max-time 2 "${ZC_URL}?action=getInfo" >/dev/null 2>&1; then
break
fi
waited=$((waited + RETRY_DELAY))
if [ $waited -ge $MAX_WAIT ]; then
log err "ZeroConf endpoint not available after ${MAX_WAIT}s — giving up"
exit 1
fi
sleep $RETRY_DELAY
done
log info "ZeroConf endpoint is up (waited ${waited}s)"
# --- Check if already primed ---
info=$(curl -sf --max-time 5 "${ZC_URL}?action=getInfo" 2>/dev/null)
active_user=$(echo "$info" | json_str activeUser)
device_name=$(echo "$info" | json_str remoteName)
if [ -n "$active_user" ]; then
log info "Already primed (device=$device_name, activeUser=$active_user) — nothing to do"
exit 0
fi
log info "Speaker '$device_name' has no active Spotify user — priming..."
# --- Get token from soundtouch-service server ---
log info "Requesting Spotify token from soundtouch-service..."
token_response=$(curl -sf --max-time 15 \
-u "${SOUNDTOUCH_USER}:${SOUNDTOUCH_PASS}" \
"${SOUNDTOUCH_URL}/mgmt/spotify/token" \
2>&1)
if [ $? -ne 0 ] || [ -z "$token_response" ]; then
log err "Failed to get token from soundtouch-service (is the server reachable?)"
exit 1
fi
access_token=$(echo "$token_response" | json_str accessToken)
user=$(echo "$token_response" | json_str username)
if [ -z "$access_token" ] || [ -z "$user" ]; then
error_msg=$(echo "$token_response" | json_str detail)
log err "soundtouch-service returned error: ${error_msg:-no token/username in response}"
exit 1
fi
log info "Got token for user $user (${access_token:0:10}...)"
# --- Prime the speaker ---
result=$(curl -sf --max-time 10 -X POST "$ZC_URL" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "action=addUser&userName=${user}&blob=${access_token}&clientKey=&tokenType=accesstoken" \
2>&1)
status=$(echo "$result" | json_num status)
status_str=$(echo "$result" | json_str statusString)
if [ "$status" != "101" ]; then
log err "addUser failed: status=$status ($status_str)"
exit 1
fi
# --- Verify (retry — speaker needs a few seconds after cold boot) ---
log info "addUser accepted (status 101) — verifying..."
for i in 1 2 3 4 5; do
sleep $((i * 2))
active_user=$(curl -sf --max-time 5 "${ZC_URL}?action=getInfo" | json_str activeUser)
if [ -n "$active_user" ]; then
log info "Speaker primed successfully (activeUser=$active_user)"
exit 0
fi
done
log warning "Speaker accepted addUser but activeUser still empty after 30s"
exit 1
+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env bash
#
# spotify-prime-speaker — Prime a Bose SoundTouch speaker for Spotify playback
#
# Activates Spotify on a SoundTouch speaker by sending an access token
# via the Spotify Connect ZeroConf endpoint (port 8200). This is the
# same mechanism the Spotify desktop app uses internally.
#
# Works standalone — no soundtouch-service, ueberboese, or other server required.
#
# Requirements: curl, jq
#
# Usage:
# ./spotify-prime-speaker SPEAKER_IP ACCESS_TOKEN
#
# Example:
# ./spotify-prime-speaker 192.168.1.143 BQDj...your_token...
#
# How to get an access token:
# - Spotify Developer Console: https://developer.spotify.com
# (create an app, use the "Get Token" button)
# - Via soundtouch-service management API: POST /mgmt/spotify/auth/init
# - Via ueberboese management API: POST /mgmt/spotify/init
# - Any Spotify OAuth Authorization Code flow with user-read-email scope
#
# Notes:
# - Access tokens expire after 1 hour (3600 seconds)
# - The speaker must be on the same network and reachable on port 8200
# - After priming, Spotify presets on the speaker should work immediately
# - Re-run after each speaker reboot (or use a server like soundtouch-service
# to automate this)
#
# How it works:
# The Bose SoundTouch speaker exposes a Spotify Connect ZeroConf API
# on port 8200. By sending an addUser request with a valid Spotify
# access token, the speaker activates its built-in Spotify Connect
# client. No encryption is needed — the token is sent as plain text,
# exactly like the Spotify desktop app does it.
#
# Related:
# - https://github.com/gesellix/Bose-SoundTouch (comprehensive toolkit)
set -euo pipefail
# --- Argument parsing ---
if [ $# -lt 2 ]; then
echo "Usage: $0 SPEAKER_IP ACCESS_TOKEN"
echo ""
echo "Prime a Bose SoundTouch speaker for Spotify playback."
echo ""
echo "Arguments:"
echo " SPEAKER_IP IP address of the SoundTouch speaker"
echo " ACCESS_TOKEN Spotify access token (starts with BQ...)"
echo ""
echo "Get a token at https://developer.spotify.com or via a server's OAuth flow."
exit 1
fi
SPEAKER_IP="$1"
TOKEN="$2"
ZC_URL="http://${SPEAKER_IP}:8200/zc"
# --- Dependency check ---
for cmd in curl jq; do
if ! command -v "$cmd" &>/dev/null; then
echo "Error: $cmd is required but not installed." >&2
exit 1
fi
done
# --- Step 1: Discover Spotify username from token ---
echo "Discovering Spotify user from token..."
ME_RESPONSE=$(curl -sf -H "Authorization: Bearer ${TOKEN}" \
https://api.spotify.com/v1/me 2>&1) || {
echo "Error: Failed to call Spotify /me API. Is the token valid?" >&2
echo " (tokens expire after 1 hour)" >&2
exit 1
}
USER=$(echo "$ME_RESPONSE" | jq -r '.id // empty')
if [ -z "$USER" ]; then
echo "Error: Could not extract user ID from Spotify response." >&2
echo "$ME_RESPONSE" >&2
exit 1
fi
echo " Spotify user: $USER"
# --- Step 2: Check current speaker status ---
echo "Checking speaker at ${SPEAKER_IP}:8200..."
INFO=$(curl -sf "${ZC_URL}?action=getInfo" 2>&1) || {
echo "Error: Could not reach speaker at ${SPEAKER_IP}:8200." >&2
echo " Is the speaker on and on the same network?" >&2
exit 1
}
ACTIVE=$(echo "$INFO" | jq -r '.activeUser // empty')
DEVICE_NAME=$(echo "$INFO" | jq -r '.remoteName // empty')
if [ -n "$DEVICE_NAME" ]; then
echo " Speaker: $DEVICE_NAME"
fi
if [ -n "$ACTIVE" ]; then
echo " Already primed (activeUser=$ACTIVE)"
echo "Done — speaker is ready for Spotify playback."
exit 0
fi
echo " No active Spotify user — priming now..."
# --- Step 3: Send addUser ---
RESULT=$(curl -sf -X POST "${ZC_URL}" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "action=addUser&userName=${USER}&blob=${TOKEN}&clientKey=&tokenType=accesstoken" \
2>&1) || {
echo "Error: addUser request failed." >&2
exit 1
}
STATUS=$(echo "$RESULT" | jq -r '.status // -1')
STATUS_STR=$(echo "$RESULT" | jq -r '.statusString // empty')
if [ "$STATUS" != "101" ]; then
echo "Error: Speaker returned status $STATUS ($STATUS_STR)" >&2
echo "$RESULT" | jq . >&2
exit 1
fi
echo " Speaker accepted the token (status 101)."
# --- Step 4: Verify ---
echo " Verifying (waiting 2 seconds)..."
sleep 2
ACTIVE=$(curl -sf "${ZC_URL}?action=getInfo" | jq -r '.activeUser // empty')
if [ -n "$ACTIVE" ]; then
echo "Done — speaker primed for Spotify (activeUser=$ACTIVE)"
else
echo "Warning: Speaker returned 101 but activeUser is still empty."
echo " The speaker may need more time. Try pressing a Spotify preset."
exit 1
fi
@@ -0,0 +1,6 @@
# /mnt/nv/BoseApp-Persistence/1/spotify-primer.conf — service connection for boot primer
# The speaker fetches a fresh Spotify token from the service at boot.
# No Spotify credentials needed on the device.
SOUNDTOUCH_URL=https://soundtouch.example.com
SOUNDTOUCH_USER=admin
SOUNDTOUCH_PASS=secret