> ## 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.

# Server setup

> Configure your personal proxy server to handle Spotify API authentication and requests from the MacroBoard

## 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.

<Note>
  The reference implementation runs on `benzhou.tech` at port 443 (HTTPS). You'll need to set up your own server with a similar configuration.
</Note>

## 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

<Warning>
  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.
</Warning>

### Obtaining your SSL certificate

After setting up your server with an SSL certificate, extract the certificate:

```bash theme={null}
# 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:

```cpp theme={null}
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](https://developer.spotify.com/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:**

```json theme={null}
{
  "title": "Song Title",
  "artist": "Artist Name",
  "album": "Album Name",
  "duration": 240,
  "progress": 45,
  "paused": false,
  "volume": 75,
  "color": [255, 87, 34]
}
```

<ParamField path="title" type="string">
  Current track title
</ParamField>

<ParamField path="artist" type="string">
  Artist name (first artist if multiple)
</ParamField>

<ParamField path="album" type="string">
  Album name
</ParamField>

<ParamField path="duration" type="integer">
  Track duration in seconds
</ParamField>

<ParamField path="progress" type="integer">
  Current playback position in seconds
</ParamField>

<ParamField path="paused" type="boolean">
  Whether playback is currently paused
</ParamField>

<ParamField path="volume" type="integer">
  Current volume level (0-100)
</ParamField>

<ParamField path="color" type="array">
  RGB color array extracted from album artwork \[R, G, B] where each value is 0-255
</ParamField>

### 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):**

```python theme={null}
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:

```javascript theme={null}
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');
});
```

<Warning>
  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
</Warning>

## Token refresh

Spotify access tokens expire after 1 hour. Implement automatic token refresh:

```javascript theme={null}
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:

```bash theme={null}
# 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`:

```json theme={null}
{
  "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:

```cpp theme={null}
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`:

```cpp theme={null}
if (!wifiClient.connect("your-domain.com", 443)) {
```

## Next steps

<CardGroup cols={2}>
  <Card title="API endpoints" icon="code" href="/api/endpoints">
    Detailed reference for all API endpoints
  </Card>

  <Card title="Troubleshooting" icon="wrench" href="/reference/troubleshooting">
    Common issues and solutions
  </Card>
</CardGroup>
