Reviving an iPad 2 as a Spotify Controller with a Milkdrop Visualizer
The Problem
I had an iPad 2 sitting in a drawer β too old for modern iOS, but the hardware still works perfectly. Rather than e-waste it, I wanted to give it a dedicated purpose: an always-on Spotify controller with a beautiful music visualizer.
The Architecture
Two repositories work together:
| Repo | Purpose | Tech |
|---|---|---|
iPad2Spotify | Backend + Spotify API bridge | Node.js, Redis, Vercel |
OldMilk | WebGL 1 Milkdrop visualizer | JavaScript, WebGL, WebAMP |
iPad2Spotify (Backend)
- Redis for session/token storage β survives iPad Safari restarts
- Spotify Web API for playback control and "now playing" metadata
- Vercel serverless functions β zero-cost hosting, global CDN
- WebAMP integration for Winamp 2 skin compatibility
OldMilk (Visualizer)
- WebGL 1 β runs on iPad 2's PowerVR SGX543MP2 (iOS 9.3.5 Safari supports it)
- Milkdrop preset parsing β loads
.milkfiles natively - AudioContext analyser node for real-time frequency data
- Zero dependencies β single HTML file, ~50 KB gzipped
Data Flow
iPad 2 Safari β Vercel (iPad2Spotify) β Redis β Spotify API
β
WebSocket β OldMilk (WebGL visualizer)
The iPad polls /now-playing every 2 seconds. When track changes, it pushes new metadata to the visualizer via WebSocket.
Key Challenges
1. iOS 9.3.5 Safari Limitations
- No ES6 modules β all code transpiled to ES5
- No
fetchpolyfill needed β usedXMLHttpRequest - WebGL 1 only β shaders written in GLSL 1.0 (
#version 100) AudioContextrequires user gesture β "Tap to Start" overlay
2. Spotify Token Refresh
Access tokens expire hourly. Solution: store refresh_token in Redis (encrypted), background cron refreshes every 50 min.
// Simplified token refresh
async function getValidToken(sessionId) {
const session = await redis.get(`spotify:${sessionId}`);
if (Date.now() > session.expiresAt - 60000) {
const newToken = await refreshToken(session.refreshToken);
await redis.set(`spotify:${sessionId}`, { ...session, ...newToken });
return newToken.accessToken;
}
return session.accessToken;
}3. Visualizer Performance on 2011 GPU
- Reduced fragment shader complexity
- Limited to 30 fps via
requestAnimationFramethrottle - Disabled expensive Milkdrop features (per-pixel motion, waveforms)
Deployment
# iPad2Spotify
cd iPad2Spotify
vercel --prod
# OldMilk (static)
cd OldMilk
vercel --prodBoth deploy to *.vercel.app subdomains. iPad 2 Safari bookmarks the visualizer URL to home screen for "app-like" experience.
Result
The iPad 2 has been running 24/7 on a wall mount for 6 months β zero crashes, ~2% battery drain per day (always plugged in).
Links
- iPad2Spotify: https://github.com/turbolego/iPad2Spotify (4 β, 3 forks)
- OldMilk: https://github.com/turbolego/OldMilk (MIT License)
- Live Demo: https://ipad2spotify.vercel.app (backend) + https://oldmilk.vercel.app (visualizer)
Built with β and nostalgia. The iPad 2 deserves a second life.