mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 08:36:13 +00:00
Prepare Spotify primer
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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.*
|
||||
@@ -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 &
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user