#!/usr/bin/env bash
# spotify-sonos-cmd4.sh
# Works with Cmd4 in either call style:
# 1) "<AccessoryName>" On get|set [0|1]
# 2) Get|Set "<AccessoryName>" "On" [0|1]
set -euo pipefail
SPOTIFY_CLIENT_ID=<SONOS_CLIENT_ID>
SPOTIFY_CLIENT_SECRET=<SONOS_SECRET>
SPOTIFY_REFRESH_TOKEN="<SONOS_TOKEN>"
# ====== REQUIRED ENV ======
# Create a Spotify app at https://developer.spotify.com, then set:
# SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET, SPOTIFY_REFRESH_TOKEN
: "${SPOTIFY_CLIENT_ID:?Set SPOTIFY_CLIENT_ID}"
: "${SPOTIFY_CLIENT_SECRET:?Set SPOTIFY_CLIENT_SECRET}"
: "${SPOTIFY_REFRESH_TOKEN:?Set SPOTIFY_REFRESH_TOKEN}"
# Sonos shows up as a Spotify Connect device. Use the *device name* as seen in the Spotify app.
: "${SPOTIFY_DEVICE_NAME:?Set SPOTIFY_DEVICE_NAME (e.g., 'Kitchen')}"
# What to play when turning ON (can be track/album/playlist/artist/show)
# Examples:
# spotify:track:3n3Ppam7vgaVa1iaRUc9Lp
# spotify:album:1ATL5GLyefJaxhQzSPVrLX
# spotify:playlist:37i9dQZF1DXcBWIGoYBM5M
: "${SPOTIFY_URI:?Set SPOTIFY_URI}"
# Optional: start position in ms
START_POSITION_MS="${START_POSITION_MS:-0}"
# Optional: volume (0-100) set after transfer; leave empty to skip
START_VOLUME="${START_VOLUME:-}"
# ====== ARGUMENT PARSING ======
A1="${1:-}"; A2="${2:-}"; A3="${3:-}"; A4="${4:-}"
lower(){ printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]'; }
if [[ "$(lower "$A1")" == "get" || "$(lower "$A1")" == "set" ]]; then
ACTION="$(lower "$A1")"; ACCESSORY="$A2"; CHAR="$(lower "$A3")"; VALUE="${A4:-}"
else
ACCESSORY="$A1"; CHAR="$(lower "$A2")"; ACTION="$(lower "$A3")"; VALUE="${A4:-}"
fi
[[ "$CHAR" == "on" || "$CHAR" == "On" ]] || { echo "Unsupported characteristic: $CHAR" >&2; exit 1; }
# ====== SPOTIFY HELPERS ======
API="https://api.spotify.com/v1"
TOKEN_ENDPOINT="https://accounts.spotify.com/api/token"
get_access_token() {
curl -sS --fail -u "${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}" \
-d grant_type=refresh_token \
--data-urlencode "refresh_token=${SPOTIFY_REFRESH_TOKEN}" \
"$TOKEN_ENDPOINT" | jq -r '.access_token'
}
api() {
local method="$1"; shift
local path="$1"; shift
local body="${1:-}"
if [[ -n "$body" ]]; then
curl -sS --fail -X "$method" "$API$path" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
--data "$body"
else
curl -sS --fail -X "$method" "$API$path" \
-H "Authorization: Bearer $ACCESS_TOKEN"
fi
}
get_device_id_by_name() {
api GET "/me/player/devices" | jq -r --arg NAME "$SPOTIFY_DEVICE_NAME" '
.devices[]? | select(.name==$NAME) | .id' | head -n1
}
transfer_and_play() {
local dev_id="$1"
# Transfer playback to the Sonos device (and auto-play)
api PUT "/me/player" "{\"device_ids\":[\"$dev_id\"],\"play\":true}" >/dev/null
# Start the requested context/track(s)
if [[ "$SPOTIFY_URI" == spotify:track:* ]]; then
api PUT "/me/player/play?device_id=$dev_id" \
"{\"uris\":[\"$SPOTIFY_URI\"],\"position_ms\":$START_POSITION_MS}" >/dev/null
else
api PUT "/me/player/play?device_id=$dev_id" \
"{\"context_uri\":\"$SPOTIFY_URI\",\"offset\":{\"position\":0},\"position_ms\":$START_POSITION_MS}" >/dev/null
fi
# Optional volume
if [[ -n "${START_VOLUME}" ]]; then
api PUT "/me/player/volume?device_id=$dev_id&volume_percent=$START_VOLUME" >/dev/null || true
fi
}
pause_playback() {
# Pause regardless of device
api PUT "/me/player/pause" "" >/dev/null || true
}
is_playing_here() {
local cur
cur="$(api GET "/me/player" || true)"
[[ -n "$cur" ]] || return 1
local dev_name playing
dev_name="$(jq -r '.device.name // empty' <<<"$cur")"
playing="$(jq -r '.is_playing // false' <<<"$cur")"
[[ "$dev_name" == "$SPOTIFY_DEVICE_NAME" && "$playing" == "true" ]]
}
# ====== RUN ======
ACCESS_TOKEN="$(get_access_token)"
case "$ACTION" in
get)
if is_playing_here; then echo 1; else echo 0; fi
;;
set)
if [[ "${VALUE:-0}" == "1" ]]; then
# Ensure device appears: if Spotify hasn’t seen the Sonos recently, ask user to start Spotify once on that speaker via the app
DEV_ID="$(get_device_id_by_name || true)"
if [[ -z "${DEV_ID:-}" ]]; then
echo "0"
echo "Spotify device '$SPOTIFY_DEVICE_NAME' not found. Make sure it is online and visible in Spotify Connect, then try again." >&2
exit 3
fi
transfer_and_play "$DEV_ID"
else
pause_playback
fi
;;
*)
echo "Unknown action: $ACTION" >&2; exit 1;;
esac
|