Skip to main content

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/) -- SpotifyClient wraps 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 via SpotifyClient::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 using crypto::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

QueryPermissionDescription
spotifyStatespotify:readCombined state: now playing, queue, devices, playlists (fetched in parallel via tokio::join!)
spotifySearch(query, limit?)spotify:readSearch Spotify tracks (default limit: 20)
spotifyManualStatusspotify:readManual-connect status (active, remainingSeconds)

GraphQL Mutations

MutationPermissionDescription
spotifyPlayback(input: SpotifyPlaybackInput!)spotify:playbackControl 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:queueAdd a track to the Spotify queue
spotifyTransferPlayback(deviceId: String!)spotify:deviceTransfer playback to a different device
spotifyPlayPlaylist(uri, name?, coverUrl?, trackCount?, playlistUrl?)spotify:playlistStart playing a playlist
spotifyCreatePlaylist(name: String!, public: Boolean)spotify:playlistCreate a new Spotify playlist
spotifyRenamePlaylist(id, name)spotify:playlistRename a Spotify playlist
spotifyDeletePlaylist(id)spotify:playlistDelete (unfollow) a Spotify playlist
spotifyAddToPlaylist(playlistId, trackUris: [String!]!)spotify:playlistAdd tracks to a Spotify playlist
spotifyRemoveFromPlaylist(playlistId, trackUri)spotify:playlistRemove a track from a Spotify playlist
startSpotifyManualspotify:workerStart manual Spotify connect (30 min TTL). Returns SpotifyManualStatus.
stopSpotifyManualspotify:workerStop manual Spotify connect. Returns Boolean.

REST Endpoints

All paths live under /v1/spotify. Bodies are snake_case.

MethodPathPermissionDescription
GET/v1/spotify/statespotify:readCombined now-playing / queue / devices / playlists state
POST/v1/spotify/playbackspotify:playbackPlayback control: play, pause, next, previous, skip_to, seek, volume, shuffle, repeat
GET/v1/spotify/queuespotify:readCurrent Spotify queue
POST/v1/spotify/queuespotify:queueAdd a track URI to the queue
GET/v1/spotify/devicesspotify:readList available playback devices
POST/v1/spotify/devicesspotify:deviceTransfer playback to a device
GET/v1/spotify/playlistsspotify:readList user playlists
POST/v1/spotify/playlistsspotify:playlistStart playing a playlist
GET/v1/spotify/searchspotify:readSearch Spotify tracks (query, optional limit)
GET/v1/spotify/manual-connectspotify:readManual-connect status (+ remaining seconds)
POST/v1/spotify/manual-connectspotify:workerStart manual Spotify polling (30 min TTL)
DELETE/v1/spotify/manual-connectspotify:workerStop manual Spotify polling

Event Types Generated

Event TypeTrigger
spotify:playPlay action
spotify:pausePause action
spotify:skipNext or skip_to action
spotify:previousPrevious action
spotify:seekSeek action
spotify:volumeVolume change (includes old/new volume)
spotify:shuffleShuffle toggle
spotify:repeatRepeat mode change
spotify:queue_addTrack added to queue
spotify:devicePlayback transferred to different device
spotify:playlistPlaylist started playing

Permissions

PermissionDescription
spotify:readRead now playing state, queue, devices, playlists, search tracks
spotify:playbackControl playback (play, pause, next, previous, seek, volume, shuffle, repeat)
spotify:queueAdd tracks to the queue
spotify:playlistManage and play playlists
spotify:deviceTransfer playback to another device
spotify:workerStart/stop the manual Spotify polling worker

Database

Spotify uses the Connections module tables for credential/token storage:

TableDatabaseDescription
app_credentialsPostgreSQLEncrypted client_id and client_secret per platform per account
channel_connectionsPostgreSQLEncrypted access_token, refresh_token, expires_at, platform_channel_id, scopes

Events generated by Spotify actions are stored in:

TableDatabaseDescription
platform_eventsTimescaleDBSpotify events (spotify:play, spotify:skip, etc.) with enriched raw JSON containing track info and triggering user

Data Flow

  1. User connects Spotify via the Connections module (OAuth flow).
  2. build_client_from_ctx() loads credentials, auto-refreshes if token expires within 5 minutes.
  3. User performs a playback action (e.g., skip) via the dashboard.
  4. The SpotifyClient method is called against the Spotify Web API.
  5. The action is enriched with current track data and triggering user info.
  6. An event is asynchronously inserted into TimescaleDB and broadcast via Redis pub/sub.
  7. Overlay "Now Playing" widget receives the event via WebSocket and updates the display.

Key Files

PathDescription
apps/api/src/graphql/spotify.rsGraphQL queries and mutations
crates/lo-spotify-api/src/Spotify Web API client
apps/api/src/db/spotify.rsCredential retrieval helpers
apps/api/src/crypto.rsToken encryption/decryption
apps/api/src/oauth.rsToken refresh persistence