Skip to content

feat(hmi): Spotify integration — PKCE OAuth + full playback controls#155

Open
rbrito02 wants to merge 2 commits into
mainfrom
feat/hmi-spotify
Open

feat(hmi): Spotify integration — PKCE OAuth + full playback controls#155
rbrito02 wants to merge 2 commits into
mainfrom
feat/hmi-spotify

Conversation

@rbrito02
Copy link
Copy Markdown
Collaborator

@rbrito02 rbrito02 commented Apr 4, 2026

Summary

  • Replaces the static hardcoded music dock ("Usseewa / Ado") with a live Spotify player backed by the Web API
  • Implements PKCE authorization code flow — no client secret needed, safe to run in the browser
  • Polls playback state every 2 seconds; progress bar interpolates smoothly between polls

New files

File Description
packages/hmi/src/hooks/useSpotify.ts PKCE OAuth flow, token storage/refresh, authenticated fetch wrapper, all playback control actions
packages/hmi/src/components/SpotifyPlayer.tsx Full player UI with three states (connect, idle, playing)
packages/hmi/.env.example Documents VITE_SPOTIFY_CLIENT_ID and VITE_SPOTIFY_REDIRECT_URI

Player features

  • Album art, track name, artist, album
  • Seekable progress bar (API call fires on pointer-up, not on every drag tick)
  • Play / Pause, Skip Previous / Next
  • Shuffle toggle (green when active)
  • Repeat cycle: off → context → track (green when active; single-track icon for track repeat)
  • Volume slider with mute toggle, debounced API calls (250 ms)
  • Graceful unauthenticated and no-active-device states

Setup

  1. Create a Spotify app at https://developer.spotify.com/dashboard
  2. Add your redirect URI (e.g. http://localhost:3000) to the app's Redirect URIs
  3. Copy packages/hmi/.env.example.env and fill in VITE_SPOTIFY_CLIENT_ID and VITE_SPOTIFY_REDIRECT_URI

Test plan

  • .env.example is present and documents both required vars
  • "Connect" button redirects to Spotify OAuth and returns to the HMI
  • Now-playing track, artist, album art display correctly
  • Play/pause, prev/next update state optimistically and poll confirms within 2 s
  • Seek bar drag updates position; API call fires only on release
  • Shuffle and repeat buttons toggle correctly (green highlight, correct icon)
  • Volume slider changes device volume; mute button toggles 0 ↔ previous level
  • "No active playback" state shown when authenticated but no device is playing
  • Logout button clears tokens and returns to connect state
  • Token refresh works silently when access token expires

rbrito02 added 2 commits April 4, 2026 19:49
…controls

Replaces the static hardcoded music dock in the HMI dashboard with a live
Spotify player. Implements PKCE authorization code flow (no client secret
required) and the Spotify Web API for real-time now-playing data.

Features:
- useSpotify hook: PKCE auth, token storage/refresh, 2-second playback polling
- SpotifyPlayer component: album art, track/artist/album display, seekable
  progress bar, play/pause, skip prev/next, shuffle toggle, repeat cycle
  (off → context → track), volume slider with mute toggle, debounced API calls
- .env.example documents VITE_SPOTIFY_CLIENT_ID and VITE_SPOTIFY_REDIRECT_URI
- Three UI states: connect prompt, no-active-device, and full player
Critical:
- useSpotify: decouple polling lifecycle from init effect — polling now starts/
  stops via a dedicated useEffect watching isAuthenticated, eliminating the
  orphaned setInterval if the component unmounts after token exchange completes
- useSpotify: rename 'next' local var in toggleShuffle to 'newShuffleState' to
  remove shadowing of the next() playback action

Important:
- SpotifyPlayer: add isLoading skeleton state to prevent flash of Connect UI
  while stored tokens are being validated on mount
- SpotifyPlayer: extract PlayerCard as a named component outside SpotifyPlayer
  so React's reconciler sees a stable type across renders
- SpotifyPlayer: fix useCallback deps — handleSeekEnd depends on spotify.seek,
  handleVolumeChange depends on spotify.setVolume (not the whole spotify object)
- SpotifyPlayer: remove stale seek-start capture of displayProgress state; read
  directly from syncRef so handleSeekStart needs no deps

Minor:
- useSpotify: replace btoa(String.fromCharCode(...spread)) with reduce to
  eliminate the spread-overflow pattern (safe at 32 bytes but non-idiomatic)
- SpotifyPlayer: add durationMs to syncRef; clamp interpolated progress at
  track duration inside the interval rather than only at render time
- SpotifyPlayer: mute toggle now restores to pre-mute volume via prevVolumeRef
  instead of hardcoding 50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant