http://localhost/lyrics/api · Base (prod): https://api.getlyricx.comPWA (app/) ──HTTPS 2 s──► PHP (cette API) ◄──HTTPS 1 POST/s── ESP32 (devices/sync.php : paroles + commandes + acks)
Start Spotify OAuth (PKCE + state), then redirect to Spotify.
curl -i "http://localhost/lyrics/api/users/login-spotify.php"
Called by Spotify with code and state. If account exists, auto-login and store app token in localStorage. If account is new, shows username setup form.
Create local app account after Spotify callback for first-time users.
username (required, 3-24, letters/numbers/_)is_public_profile (required, 1 or 0){
"ok": true,
"user": { "id": 1, "username": "my_name", "is_public_profile": true },
"auth_token": "lx_...",
"redirect": "http://localhost/lyrics/login.php?created=1"
}
curl -H "Authorization: Bearer lx_YOUR_TOKEN" "http://localhost/lyrics/api/users/me.php"
{ "ok": true, "user": { "id": 1, "username": "my_name", "spotify_display_name": "...", ... } }
Now playing + synced lyrics + album art grid (art = 16, 32 or 64) for a user. Used by the profile page. The ESP32 gets the same payload inside devices/sync.php. Logic lives in lyrics/_lyrics.php (lyrics_payload()).
The single point of contact of a display. Uploads acks / state / now / stats, downloads pairing status, queued commands, the "frames wanted" flag and the lyrics payload of the paired account.
curl -X POST "http://localhost/lyrics/api/devices/sync.php" -H "Content-Type: application/json" -d '{
"device_id":"LX-A82F91","secret":"…","firmware":"2.0.0","model":"matrix-128x64","ip":"192.168.1.42","rssi":-52,
"have":"","art":64,
"acks":[{"id":123,"ok":true}],
"state":{"brightness":92, "...":"..."}, "now":{"playing":true, "...":"..."}
}'
// not paired yet
{"success":false,"error":"Not paired","device":{"paired":false,"claim_code":"7K3M2A","commands":[],"frames":false,"server_time":1758367200}}
// paired: lyrics payload + device block
{"success":true,"track_id":"…","now_playing":{…},"lyrics":[…],
"device":{"paired":true,"user":{"id":42,"username":"noah"},"name":"Salon",
"commands":[{"id":125,"command":"brightness","value":70}],"frames":false,"server_time":1758367200}}
// refused
401 {"success":false,"error":"bad secret"}
Raw body of 16 384 bytes (128×64 RGB565 LE): the current framebuffer. Sent ~2×/s only while sync.php answers "frames":true.
Displays linked to the account with their last state. The app polls it every 2 s.
curl -H "Authorization: Bearer lx_YOUR_TOKEN" "http://localhost/lyrics/api/devices/mine.php"
{
"ok": true, "count": 1, "server_time": 1758367200,
"devices": [{
"device_id": "LX-A82F91", "name": "Salon", "online": true, "firmware": "2.0.0", "model": "matrix-128x64",
"ip": "192.168.1.42", "rssi": -52, "last_seen": "2026-09-20 12:00:00", "created_at": "2026-09-20 11:00:00",
"state": { "brightness": 70, "style": 2, ... }, "sys": { "ssid": "...", ... },
"now": { "playing": true, "track": "...", "artist": "...", "progress": 45210, "duration": 271600, ... },
"stats": { "totalMin": 1234, ... }
}]
}
One display (same shape as an item of mine.php).
Queues a command for the display and waits for its ack (default 8 s, max 12 s; timeout_ms: 0 = fire and forget → 202). The display picks it up at its next sync (≤ 1 s).
curl -X POST "http://localhost/lyrics/api/devices/command.php" \
-H "Authorization: Bearer lx_YOUR_TOKEN" -H "Content-Type: application/json" \
-d '{"device_id":"LX-A82F91","command":"brightness","value":70}'
200 {"ok":true,"ack":{"id":125,"ok":true,"latency_ms":640}}
200 {"ok":false,"error":"brightness: integer expected","ack":{"id":126,"ok":false,"error":"…","latency_ms":700}}
202 {"ok":true,"queued":true,"ack":{"id":127,"status":"pending"}}
404 {"ok":false,"error":"device offline","last_seen":"2026-09-20 12:00:00"}
504 {"ok":false,"error":"timeout","ack":{"id":128,"status":"sent"}}
| command | value | effect |
|---|---|---|
brightness | 5…100 | panel brightness |
style | 0…12 | lyric style |
autorotate, accentauto, showprogress, progresswave, showbreaks, night | bool | toggles |
lead | −1000…2000 | lyric offset (ms) |
accent | "#RRGGBB" | manual accent color |
titlestyle 0…2, transition 0…2, nolyrics 0…1, idlemode 0…2, clockstyle 0…5, colororder 0…5 | int | screens |
pauseidle | 0…120 | minutes before idle screen (0 = never) |
nightstart, nightend | 0…23 | night mode window |
nightbri | 0…60 | night brightness (0 = off) |
location | {"lat":50.85,"lon":4.35} | weather |
set | { "brightness": 70, "style": 2, … } | batch of settings |
colortest | 1 / 2 / 3 | panel color test patterns |
statsreset, reboot, wifireset, identify, get_state, get_stats | — | actions |
ota | "https://…/firmware.bin" | download + flash + reboot |
api | "http://…/lyrics/api" or "" | change API base (NVS) + reboot |
Link the display showing this pairing code to the account. The display learns it at its next sync and switches to lyrics.
curl -X POST "http://localhost/lyrics/api/devices/claim.php" \
-H "Authorization: Bearer lx_YOUR_TOKEN" -H "Content-Type: application/json" \
-d '{"code":"7K3M2A","name":"Salon"}'
200 { "ok": true, "device": { "device_id": "LX-A82F91", "name": "Salon", "online": true, ... } }
404 { "ok": false, "error": "Invalid or expired pairing code. ..." }
409 { "ok": false, "error": "This Matrix is not connected right now. ..." }
410 { "ok": false, "error": "This pairing code has expired. ..." }
Body {"device_id":"LX-…"}. Unlinks the display; at its next sync it shows a new pairing code. Pending commands are dropped.
Body {"device_id":"LX-…","name":"Chambre"} (1–64 chars).
Live LED preview. want=1 asks the display to stream its framebuffer for the next 15 s (the app calls this every 0.5 s). Returns 200 + 16 384 bytes (RGB565 LE) or 204 when no fresh frame (< 10 s) exists.
// Save token
localStorage.setItem('lyricx_auth_token', authToken);
// Use token
const token = localStorage.getItem('lyricx_auth_token');
const me = await fetch('/lyrics/api/users/me.php', {
headers: { Authorization: `Bearer ${token}` }
}).then(r => r.json());
// The app (app/) reads the same token, polls devices/mine.php and sends devices/command.php.
# simulated display (no ESP32 needed) — prints "PAIR CODE XXXXXX" and executes commands
API_BASE=http://localhost/lyrics/api node tools/fake-device.js
# full automated chain: register → claim → command → ack → state → frame → unlink
API_BASE=http://localhost/lyrics/api TOKEN=lx_test node tools/smoke.js
Spotify redirect relay endpoint (prod domain) forwarding callback query params to local callback target.