Spotify / Music
Overview
The Spotify module integrates with the Spotify Web API to provide "Now Playing" overlays and a full dashboard music player. It supports playback control (play, pause, next, previous, seek, volume, shuffle, repeat), queue management, playlist browsing and playback, device selection and transfer, and track search. Spotify credentials are stored encrypted per-account. The module includes automatic token refresh with a 5-minute buffer before expiry. All playback actions are logged as events in TimescaleDB and broadcast via Redis pub/sub for real-time overlay updates. Smart polling runs at 3-second intervals when playing and 15-second intervals when paused.
Architecture
Backend
- GraphQL (
apps/api/src/graphql/spotify.rs) -- Queries for combined Spotify state and track search. Mutations for playback control, queue management, playlist playback, and device transfer. - Crate (
crates/lo-spotify-api/src/) --SpotifyClientwraps the Spotify Web API with methods for now_playing, queue, devices, playlists, search, play, pause, next, previous, seek, volume, shuffle, repeat, add_to_queue, transfer_playback, play_playlist. Also handles token refresh viaSpotifyClient::refresh_token(). - Credentials -- Stored in PostgreSQL via
db::spotify::get_spotify_credentials(). Client ID, client secret, access token, and refresh token are all encrypted at rest usingcrypto::encrypt/decrypt. Connection is managed through the Connections module (Spotify as a platform). - Event Logging -- Every playback action emits an event (e.g.,
spotify:play,spotify:pause,spotify:skip) to TimescaleDB and broadcasts it via Redis pub/sub asynchronously.
Frontend
- Dashboard music player with now playing display, playback controls, queue view, playlist browser, and device selector.
- Overlay widget displays current track info, album art, and progress bar.
- Next.js API proxy routes call GraphQL internally.
API
GraphQL Queries
| Query | Permission | Description |
|---|---|---|
spotifyState | spotify:read | Combined state: now playing, queue, devices, playlists (fetched in parallel via tokio::join!) |
spotifySearch(query, limit?) | spotify:read | Search Spotify tracks (default limit: 20) |
spotifyManualStatus | spotify:read | Manual-connect status (active, remainingSeconds) |
GraphQL Mutations
| Mutation | Permission | Description |
|---|---|---|
spotifyPlayback(input: SpotifyPlaybackInput!) | spotify:playback | Control playback. Actions: play (optional track URI), pause, next, previous, seek (position in ms), volume (0-100), shuffle (bool), repeat (off/context/track). (Schema action enum: play, pause, next, previous, seek, volume, shuffle, repeat.) |
spotifyQueue(uri: String!) | spotify:queue | Add a track to the Spotify queue |
spotifyTransferPlayback(deviceId: String!) | spotify:device | Transfer playback to a different device |
spotifyPlayPlaylist(uri, name?, coverUrl?, trackCount?, playlistUrl?) | spotify:playlist | Start playing a playlist |
spotifyCreatePlaylist(name: String!, public: Boolean) | spotify:playlist | Create a new Spotify playlist |
spotifyRenamePlaylist(id, name) | spotify:playlist | Rename a Spotify playlist |
spotifyDeletePlaylist(id) | spotify:playlist | Delete (unfollow) a Spotify playlist |
spotifyAddToPlaylist(playlistId, trackUris: [String!]!) | spotify:playlist | Add tracks to a Spotify playlist |
spotifyRemoveFromPlaylist(playlistId, trackUri) | spotify:playlist | Remove a track from a Spotify playlist |
startSpotifyManual | spotify:worker | Start manual Spotify connect (30 min TTL). Returns SpotifyManualStatus. |
stopSpotifyManual | spotify:worker | Stop manual Spotify connect. Returns Boolean. |
REST Endpoints
All paths live under /v1/spotify. Bodies are snake_case.
| Method | Path | Permission | Description |
|---|---|---|---|
GET | /v1/spotify/state | spotify:read | Combined now-playing / queue / devices / playlists state |
POST | /v1/spotify/playback | spotify:playback | Playback control: play, pause, next, previous, skip_to, seek, volume, shuffle, repeat |
GET | /v1/spotify/queue | spotify:read | Current Spotify queue |
POST | /v1/spotify/queue | spotify:queue | Add a track URI to the queue |
GET | /v1/spotify/devices | spotify:read | List available playback devices |
POST | /v1/spotify/devices | spotify:device | Transfer playback to a device |
GET | /v1/spotify/playlists | spotify:read | List user playlists |
POST | /v1/spotify/playlists | spotify:playlist | Start playing a playlist |
GET | /v1/spotify/search | spotify:read | Search Spotify tracks (query, optional limit) |
GET | /v1/spotify/manual-connect | spotify:read | Manual-connect status (+ remaining seconds) |
POST | /v1/spotify/manual-connect | spotify:worker | Start manual Spotify polling (30 min TTL) |
DELETE | /v1/spotify/manual-connect | spotify:worker | Stop manual Spotify polling |
Event Types Generated
| Event Type | Trigger |
|---|---|
spotify:play | Play action |
spotify:pause | Pause action |
spotify:skip | Next or skip_to action |
spotify:previous | Previous action |
spotify:seek | Seek action |
spotify:volume | Volume change (includes old/new volume) |
spotify:shuffle | Shuffle toggle |
spotify:repeat | Repeat mode change |
spotify:queue_add | Track added to queue |
spotify:device | Playback transferred to different device |
spotify:playlist | Playlist started playing |
Permissions
| Permission | Description |
|---|---|
spotify:read | Read now playing state, queue, devices, playlists, search tracks |
spotify:playback | Control playback (play, pause, next, previous, seek, volume, shuffle, repeat) |
spotify:queue | Add tracks to the queue |
spotify:playlist | Manage and play playlists |
spotify:device | Transfer playback to another device |
spotify:worker | Start/stop the manual Spotify polling worker |
Database
Spotify uses the Connections module tables for credential/token storage:
| Table | Database | Description |
|---|---|---|
app_credentials | PostgreSQL | Encrypted client_id and client_secret per platform per account |
channel_connections | PostgreSQL | Encrypted access_token, refresh_token, expires_at, platform_channel_id, scopes |
Events generated by Spotify actions are stored in:
| Table | Database | Description |
|---|---|---|
platform_events | TimescaleDB | Spotify events (spotify:play, spotify:skip, etc.) with enriched raw JSON containing track info and triggering user |
Data Flow
- User connects Spotify via the Connections module (OAuth flow).
build_client_from_ctx()loads credentials, auto-refreshes if token expires within 5 minutes.- User performs a playback action (e.g., skip) via the dashboard.
- The
SpotifyClientmethod is called against the Spotify Web API. - The action is enriched with current track data and triggering user info.
- An event is asynchronously inserted into TimescaleDB and broadcast via Redis pub/sub.
- Overlay "Now Playing" widget receives the event via WebSocket and updates the display.
Key Files
| Path | Description |
|---|---|
apps/api/src/graphql/spotify.rs | GraphQL queries and mutations |
crates/lo-spotify-api/src/ | Spotify Web API client |
apps/api/src/db/spotify.rs | Credential retrieval helpers |
apps/api/src/crypto.rs | Token encryption/decryption |
apps/api/src/oauth.rs | Token refresh persistence |