Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/benz206/SpotifyMacroBoard/llms.txt

Use this file to discover all available pages before exploring further.

Overview

The Spotify MacroBoard requires a personal server that acts as a proxy between the ESP32 and the Spotify Web API. This server handles OAuth authentication, token refresh, and API requests on behalf of the MacroBoard.
The reference implementation runs on benzhou.tech at port 443 (HTTPS). You’ll need to set up your own server with a similar configuration.

Why a proxy server?

The ESP32 has limited computational resources and memory, making it impractical to:
  • Implement OAuth 2.0 authentication flow
  • Store and refresh access tokens
  • Parse complex JSON responses from Spotify API
  • Handle API rate limiting and retries
The proxy server handles these complexities, allowing the ESP32 to make simple GET requests with minimal processing.

Server requirements

Domain and SSL certificate

Your server must be accessible via HTTPS with a valid SSL certificate:
  • Domain name: The ESP32 connects to a hardcoded domain (e.g., benzhou.tech)
  • SSL certificate: Required for secure HTTPS communication
  • Port 443: Standard HTTPS port
The ESP32 validates the server’s SSL certificate against the benzServerCert constant in SampleCredentials.h. If you use a self-signed certificate, you must include the full certificate chain.

Obtaining your SSL certificate

After setting up your server with an SSL certificate, extract the certificate:
# Get the certificate from your server
openssl s_client -connect your-domain.com:443 -showcerts < /dev/null 2>/dev/null | \
  openssl x509 -outform PEM > server-cert.pem

# View the certificate content
cat server-cert.pem
Copy the certificate content (including -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----) into your SampleCredentials.h file:
const char *benzServerCert =
    "-----BEGIN CERTIFICATE-----\n"
    "MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh\n"
    "MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n"
    // ... rest of certificate ...
    "-----END CERTIFICATE-----";

Spotify API setup

Before implementing your server, register your application with Spotify:

1. Create a Spotify app

  1. Go to Spotify Developer Dashboard
  2. Log in with your Spotify account
  3. Click “Create app”
  4. Fill in the app details:
    • App name: Spotify MacroBoard Server
    • App description: Personal proxy server for ESP32 MacroBoard
    • Redirect URI: https://your-domain.com/callback
  5. Accept the terms and click “Create”

2. Get your credentials

After creating the app:
  1. Click on your app in the dashboard
  2. Go to “Settings”
  3. Note your Client ID and Client Secret
  4. These will be used in your server implementation

3. Required scopes

Your server needs to request the following OAuth scopes:
  • user-read-playback-state - Get current playback information
  • user-modify-playback-state - Control playback (play, pause, skip, etc.)
  • user-read-currently-playing - Get currently playing track

Server implementation

Your server must implement two endpoints that match the ESP32’s expectations:

Endpoint 1: Manage playback state

GET /api/manageState/{password}/{action}
This endpoint receives control commands from the MacroBoard and forwards them to Spotify. Implementation requirements:
  1. Validate the password parameter
  2. Map the action to the appropriate Spotify API endpoint:
    • playPause → PUT /me/player/pause or /me/player/play
    • skip → POST /me/player/next
    • back → POST /me/player/previous
    • vinc → PUT /me/player/volume (increase by 10%)
    • vdec → PUT /me/player/volume (decrease by 10%)
    • shuffle → PUT /me/player/shuffle
    • loop → PUT /me/player/repeat
  3. Make the authenticated request to Spotify API
  4. Return success/failure status

Endpoint 2: Get current playback

GET /api/getCurrent/{password}
This endpoint returns the current playback state formatted for the ESP32. Response format:
{
  "title": "Song Title",
  "artist": "Artist Name",
  "album": "Album Name",
  "duration": 240,
  "progress": 45,
  "paused": false,
  "volume": 75,
  "color": [255, 87, 34]
}
title
string
Current track title
artist
string
Artist name (first artist if multiple)
album
string
Album name
duration
integer
Track duration in seconds
progress
integer
Current playback position in seconds
paused
boolean
Whether playback is currently paused
volume
integer
Current volume level (0-100)
color
array
RGB color array extracted from album artwork [R, G, B] where each value is 0-255

Album art color extraction

The color field provides an RGB array that represents the dominant color from the album artwork. The ESP32 uses this to set the RGB LED strip color, creating ambient lighting that matches the current track. Implementation approaches:
  1. Image processing library: Use a library like Pillow (Python) or Sharp (Node.js) to analyze the album art
  2. Color quantization: Extract the dominant color using k-means clustering or similar algorithms
  3. Caching: Cache colors by album ID to avoid reprocessing
Example implementation (Python with Pillow):
from PIL import Image
import requests
from io import BytesIO
import colorsys

def get_dominant_color(image_url):
    response = requests.get(image_url)
    img = Image.open(BytesIO(response.content))
    img = img.resize((150, 150))  # Resize for faster processing
    
    # Get color palette
    colors = img.getcolors(150 * 150)
    
    # Find most common color
    dominant = max(colors, key=lambda x: x[0])[1]
    
    return list(dominant)[:3]  # Return [R, G, B]

Example server (Node.js)

Here’s a basic implementation using Node.js and Express:
const express = require('express');
const axios = require('axios');
const app = express();

const PASSWORD = 'your-password-here';
let accessToken = 'your-spotify-access-token';
let refreshToken = 'your-spotify-refresh-token';

// Middleware to validate password
function validatePassword(req, res, next) {
  if (req.params.password !== PASSWORD) {
    return res.status(401).json({ error: 'Invalid password' });
  }
  next();
}

// Manage playback state
app.get('/api/manageState/:password/:action', validatePassword, async (req, res) => {
  const { action } = req.params;
  
  try {
    let endpoint, method = 'PUT', body = {};
    
    switch(action) {
      case 'playPause':
        // Check current state and toggle
        const state = await getCurrentPlayback();
        endpoint = state.is_playing ? 'pause' : 'play';
        break;
      case 'skip':
        endpoint = 'next';
        method = 'POST';
        break;
      case 'back':
        endpoint = 'previous';
        method = 'POST';
        break;
      case 'vinc':
        const currentVol = await getCurrentVolume();
        endpoint = 'volume';
        body = { volume_percent: Math.min(100, currentVol + 10) };
        break;
      case 'vdec':
        const currentVol2 = await getCurrentVolume();
        endpoint = 'volume';
        body = { volume_percent: Math.max(0, currentVol2 - 10) };
        break;
      case 'shuffle':
        endpoint = 'shuffle';
        body = { state: true };
        break;
      case 'loop':
        endpoint = 'repeat';
        body = { state: 'track' };
        break;
    }
    
    await axios({
      method,
      url: `https://api.spotify.com/v1/me/player/${endpoint}`,
      headers: { 'Authorization': `Bearer ${accessToken}` },
      data: body
    });
    
    res.json({ success: true });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// Get current playback
app.get('/api/getCurrent/:password', validatePassword, async (req, res) => {
  try {
    const response = await axios.get('https://api.spotify.com/v1/me/player', {
      headers: { 'Authorization': `Bearer ${accessToken}` }
    });
    
    const data = response.data;
    const color = await getDominantColor(data.item.album.images[0].url);
    
    res.json({
      title: data.item.name,
      artist: data.item.artists[0].name,
      album: data.item.album.name,
      duration: Math.floor(data.item.duration_ms / 1000),
      progress: Math.floor(data.progress_ms / 1000),
      paused: !data.is_playing,
      volume: data.device.volume_percent,
      color: color
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.listen(443, () => {
  console.log('Server running on port 443');
});
This is a simplified example. A production server should include:
  • Automatic token refresh logic
  • Error handling and retries
  • Rate limiting
  • Logging and monitoring
  • Environment variables for credentials

Token refresh

Spotify access tokens expire after 1 hour. Implement automatic token refresh:
async function refreshAccessToken() {
  const response = await axios.post('https://accounts.spotify.com/api/token', 
    new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET
    })
  );
  
  accessToken = response.data.access_token;
  
  // Schedule next refresh before expiration
  setTimeout(refreshAccessToken, 55 * 60 * 1000); // 55 minutes
}

Testing your server

Before connecting the ESP32, test your endpoints:
# Test getCurrent endpoint
curl https://your-domain.com/api/getCurrent/your-password

# Test playPause
curl https://your-domain.com/api/manageState/your-password/playPause

# Test skip
curl https://your-domain.com/api/manageState/your-password/skip
Expected response from getCurrent:
{
  "title": "Bohemian Rhapsody",
  "artist": "Queen",
  "album": "A Night at the Opera",
  "duration": 354,
  "progress": 120,
  "paused": false,
  "volume": 80,
  "color": [45, 52, 71]
}

Updating ESP32 configuration

After setting up your server, update your SampleCredentials.h file:
const String PASSWORD = "your-secure-password";

const char SSID[] = "Your-WiFi-SSID";
const char SSID_PASS[] = "your-wifi-password";

const char *benzServerCert =
    "-----BEGIN CERTIFICATE-----\n"
    "YOUR-CERTIFICATE-CONTENT-HERE\n"
    "-----END CERTIFICATE-----";
If you’re using a different domain, you’ll also need to modify the hardcoded domain in src.ino:195 and src.ino:210:
if (!wifiClient.connect("your-domain.com", 443)) {

Next steps

API endpoints

Detailed reference for all API endpoints

Troubleshooting

Common issues and solutions

Build docs developers (and LLMs) love