A serverless music player built with Next.js and Cloudflare R2.
Live: music.shuttlelab.org
- Multiple formats: MP3, M4A, WAV, OGG, FLAC, MP4
- Shuffle and sequential playback
- Seekable progress bar
- Search
- Responsive layout (mobile & desktop)
- Cloud storage via Cloudflare R2
- Smart cache (Cache API) and prefetch for fast/offline playback
- Frontend: Next.js 15, React 19, TypeScript, Tailwind CSS
- Audio: HTML5 Audio, Range requests (streaming), Cache API
- Backend: Next.js API Routes, Cloudflare R2 (S3-compatible)
music-shuttle/
├── app/
│ ├── api/audio/route.ts # GET /api/audio?key=...
│ ├── api/list/route.ts # GET /api/list
│ ├── layout.tsx
│ ├── page.tsx
│ └── globals.css
├── components/player/MusicPlayer.tsx
├── lib/
│ ├── hooks/ # useMusicList, useAudioPlayer, useAudioCache, useAudioPrefetch
│ └── utils.ts
├── .env.local.example
└── wrangler.toml # R2 binding for Cloudflare
Requirements: Node.js 18+, npm/pnpm, Cloudflare account, R2 bucket with audio files.
git clone <your-repo-url>
cd music-shuttle
npm install
npm run devOpen http://localhost:3000.
Note: R2 APIs need Cloudflare; for full behavior, deploy to Cloudflare Pages.
-
Connect repo to Cloudflare Pages.
-
Build command:
npx @cloudflare/next-on-pages@1 -
Output directory:
.vercel/output/static -
In
wrangler.toml, set R2 binding:compatibility_flags = ["nodejs_compat"] [[r2_buckets]] binding = "R2_BUCKET" bucket_name = "your-bucket-name"
No API keys needed; R2 is accessed via bindings.
Full guide: CLOUDFLARE_DEPLOYMENT.md
| Command | Description |
|---|---|
npm run dev |
Start dev server |
npm run build |
Local build |
npm run pages:build |
Build for Cloudflare Pages |
npm run lint |
Lint |
- List:
GET /api/list— optional:?prefix=...,?limit=N,?demo=1,?debug=1 - Audio:
GET /api/audio?key=filename.mp3— supports Range for streaming
- Empty list? Check R2 binding in
wrangler.toml, bucket name, and that files have supported extensions (.mp3, .m4a, .wav, .ogg, .flac, .mp4). - Two requests per track? One is streaming (instant play), one is full prefetch for cache; this is intentional.
- Clear cache: DevTools → Application → Cache Storage → delete
music-shuttle-v1, or runcaches.delete('music-shuttle-v1')in console.
- Fork the repo
- Create a branch:
git checkout -b feature/your-feature - Commit and push, then open a Pull Request
MIT