Table of Contents |
---|
Overview
Create a Spotify App & Add a Redirect URI
Go to the Spotify Developer Dashboard → create an app. https://developer.spotify.com/dashboard
In the app Settings, add a Redirect URI you can receive in a browser, http://127.0.0.1:8888/callback, then Save.
- Record the Client ID and Client Secret.
2) Authorize your account (one-time)
Open this URL in your browser (replace YOUR_CLIENT_ID
and URL-encode the redirect URI):
...
Get Refresh Token
Run the following script using the following parameters:
Code Block | ||
---|---|---|
| ||
SPOTIFY_CLIENT_ID=... SPOTIFY_CLIENT_SECRET=... ./get-spotify-refresh-token.sh |
Code Block | ||||
---|---|---|---|---|
| ||||
#!/usr/bin/env bash
# get-spotify-refresh-token.sh
# One-time helper to obtain a Spotify REFRESH TOKEN for the Web API.
#
# Usage (env vars OR flags):
# SPOTIFY_CLIENT_ID=... SPOTIFY_CLIENT_SECRET=... ./get-spotify-refresh-token.sh
# ./get-spotify-refresh-token.sh --client-id=... --client-secret=...
#
# Options:
# --client-id=ID
# --client-secret=SECRET
# --redirect-uri=URI (default: http://127.0.0.1:8888/callback)
# --scopes="space separated scopes"
# --pkce (use Authorization Code with PKCE; no client secret needed)
#
# Output on success:
# export SPOTIFY_CLIENT_ID="..."
# export SPOTIFY_CLIENT_SECRET="..." # omitted in --pkce mode
# export SPOTIFY_REFRESH_TOKEN="..."
#
# Requirements: bash, curl, jq, python3. (openssl recommended)
set -euo pipefail
# -------- defaults --------
CLIENT_ID="${SPOTIFY_CLIENT_ID:-}"
CLIENT_SECRET="${SPOTIFY_CLIENT_SECRET:-}"
REDIRECT_URI_DEFAULT="http://127.0.0.1:8888/callback"
REDIRECT_URI="${SPOTIFY_REDIRECT_URI:-$REDIRECT_URI_DEFAULT}"
SCOPES="${SPOTIFY_SCOPES:-user-modify-playback-state user-read-playback-state}"
USE_PKCE="false"
# -------- args --------
for arg in "$@"; do
case "$arg" in
--client-id=*) CLIENT_ID="${arg#*=}";;
--client-secret=*) CLIENT_SECRET="${arg#*=}";;
--redirect-uri=*) REDIRECT_URI="${arg#*=}";;
--scopes=*) SCOPES="${arg#*=}";;
--pkce) USE_PKCE="true";;
--help|-h)
sed -n '1,60p' "$0"; exit 0;;
*)
echo "Unknown option: $arg" >&2; exit 2;;
esac
done
need() { if [[ -z "${!1:-}" ]]; then echo "Missing required value: $1" >&2; exit 2; fi; }
dep_check() {
for cmd in curl jq python3; do
command -v "$cmd" >/dev/null 2>&1 || { echo "Missing dependency: $cmd" >&2; exit 2; }
done
}
dep_check
# URL-encode via Python
urlencode() {
python3 - "$1" <<'PY' 2>/dev/null
import sys, urllib.parse
print(urllib.parse.quote(sys.argv[1], safe=''))
PY
}
# Parse host/port from redirect for the tiny local server
parse_host_port() {
python3 - "$1" <<'PY' 2>/dev/null
import sys, urllib.parse
u = urllib.parse.urlparse(sys.argv[1])
host = u.hostname or "127.0.0.1"
port = u.port or 8888
print(f"{host} {port}")
PY
}
# PKCE helpers
b64url() {
python3 - <<'PY' 2>/dev/null
import sys, base64
print(base64.urlsafe_b64encode(sys.stdin.read().encode()).decode().rstrip("="))
PY
}
sha256_hex() {
python3 - <<'PY' 2>/dev/null
import sys, hashlib
print(hashlib.sha256(sys.stdin.read().encode()).digest().hex())
PY
}
# -------- prepare auth request --------
need CLIENT_ID
if [[ "$USE_PKCE" != "true" ]]; then
need CLIENT_SECRET
fi
if command -v openssl >/dev/null 2>&1; then
STATE="$(openssl rand -hex 16)"
else
STATE="state$(date +%s)"
fi
if [[ "$USE_PKCE" == "true" ]]; then
# RFC 7636: code_verifier 43-128 chars
if command -v openssl >/dev/null 2>&1; then
CODE_VERIFIER="$(openssl rand -hex 32)"
else
CODE_VERIFIER="cv$(date +%s)$(od -An -N8 -tx1 /dev/urandom 2>/dev/null | tr -d ' \n')"
fi
# code_challenge = BASE64URL-ENCODE(SHA256(code_verifier))
CODE_CHALLENGE="$(printf '%s' "$CODE_VERIFIER" | python3 - <<'PY'
import sys, hashlib, base64
cv = sys.stdin.read().encode()
print(base64.urlsafe_b64encode(hashlib.sha256(cv).digest()).decode().rstrip("="))
PY
)"
fi
ENC_REDIRECT="$(urlencode "$REDIRECT_URI")"
ENC_SCOPES="$(printf %s "$SCOPES" | sed 's/ /%20/g')"
AUTH_URL="https://accounts |
...
.spotify.com/authorize?client_id= |
...
${CLIENT_ID}&response_type=code&redirect_uri= |
...
Log in, approve. Spotify will redirect to:
http://localhost/callback?code=AUTH_CODE&state=xyz123
...
${ENC_REDIRECT}&scope=${ENC_SCOPES}&state=${STATE}&show_dialog=true"
if [[ "$USE_PKCE" == "true" ]]; then
AUTH_URL="${AUTH_URL}&code_challenge_method=S256&code_challenge=$(urlencode "$CODE_CHALLENGE")"
fi
# -------- open browser --------
echo "Opening browser for Spotify consent…"
if command -v xdg-open >/dev/null; then
xdg-open "$AUTH_URL" >/dev/null 2>&1 || true
elif command -v open >/dev/null; then
open "$AUTH_URL" >/dev/null 2>&1 || true
else
echo "$AUTH_URL"
fi
# -------- tiny local callback server (single request) --------
read -r HOST PORT < <(parse_host_port "$REDIRECT_URI")
echo "Listening on ${HOST}:${PORT} for the callback (Ctrl+C to abort)…"
callback_json="$(
python3 - "$HOST" "$PORT" <<'PY' 2>/dev/null || true
import http.server, socketserver, sys, json
from urllib.parse import urlparse, parse_qs
HOST, PORT = sys.argv[1], int(sys.argv[2])
capt = {"code": "", "state": ""}
class H(http.server.BaseHTTPRequestHandler):
def do_GET(self):
qs = parse_qs(urlparse(self.path).query)
capt["code"] = (qs.get("code") or [""])[0]
capt["state"] = (qs.get("state") or [""])[0]
self.send_response(200)
self.send_header("Content-Type","text/html")
self.end_headers()
self.wfile.write(b"<html><body><h2>OK</h2>You can close this tab.</body></html>")
def log_message(self, *args): pass
with socketserver.TCPServer((HOST, PORT), H) as httpd:
httpd.timeout = 180
httpd.handle_request()
print(json.dumps(capt))
PY
)"
CODE="$(printf '%s' "$callback_json" | jq -r '.code // empty')"
GOT_STATE="$(printf '%s' "$callback_json" | jq -r '.state // empty')"
if [[ -z "$CODE" ]]; then
echo "No authorization code received." >&2
exit 3
fi
if [[ "$GOT_STATE" != "$STATE" ]]; then
echo "State mismatch. Aborting." >&2
exit 3
fi
# -------- exchange code for tokens --------
TOKEN_URL="https://accounts.spotify.com/api/token"
if [[ "$USE_PKCE" == "true" ]]; then
TOKENS_JSON="$(
curl -sS -X POST "$TOKEN_URL" \
-d grant_type=authorization_code \
--data-urlencode "code=${CODE}" \
--data-urlencode "redirect_uri=${REDIRECT_URI}" \
--data-urlencode "client_id=${CLIENT_ID}" \
--data-urlencode "code_verifier=${CODE_VERIFIER}"
)"
else
TOKENS_JSON="$(
curl -sS -X POST "$TOKEN_URL" \
-u "${CLIENT_ID}:${CLIENT_SECRET}" \
-d grant_type=authorization_code \
--data-urlencode "code=${CODE}" \
--data-urlencode "redirect_uri=${REDIRECT_URI}"
)"
fi
REFRESH="$(printf '%s' "$TOKENS_JSON" | jq -r '.refresh_token // empty')"
ACCESS="$(printf '%s' "$TOKENS_JSON" | jq -r '.access_token // empty')"
ERR_DESC="$(printf '%s' "$TOKENS_JSON" | jq -r '.error_description // empty')"
if [[ -z "$REFRESH" ]]; then
echo "Did not receive refresh_token. Full response follows:" >&2
echo "$TOKENS_JSON" >&2
[[ -n "$ERR_DESC" ]] && echo "Hint: $ERR_DESC" >&2
exit 4
fi
# -------- success output --------
cat <<EOF
Success! Save these to the environment that runs Homebridge:
export SPOTIFY_CLIENT_ID="$CLIENT_ID"
EOF
if [[ "$USE_PKCE" != "true" ]]; then
echo "export SPOTIFY_CLIENT_SECRET=\"$CLIENT_SECRET\""
fi
cat <<EOF
export SPOTIFY_REFRESH_TOKEN="$REFRESH"
Keep them secret. You can now use this refresh token in your playback script.
EOF |