A modern, feature-rich internet radio player built with Next.js 16 and React 19. Stream 40,000+ stations with real-time lyrics, audio visualizers, a 5-band equalizer, podcast support, and audiobooks — all in your browser.
Features • Getting Started • Architecture • Contributing • Roadmap
- 40,000+ stations from Radio Browser API — browse by genre, country, trending, or local
- ICY metadata extraction — real-time "Now Playing" track info from stream headers
- Station queue — queue up stations and move between them seamlessly
- Stall recovery — automatic retry logic when streams buffer or drop
- Media Session API — OS-level controls (lock screen, headphone buttons, media keys)
- Wake Lock — prevents screen from sleeping during playback
- Sleep timer — auto-stop playback after a configurable duration
- Album artwork — automatic lookup via iTunes Search API with strict Jaro-distance matching and graceful fallback
- Audio visualizers — ferrofluid, spiral, and circular renderers using Web Audio FFT analysis
- Audio-reactive background — dynamic background that pulses with the music (works with or without audio effects enabled — uses a synthesized ambient pulse as fallback)
- Theater mode — immersive full-screen playback with parallax album art background
- Concert ticker — animated scrolling banner showing upcoming Bandsintown shows in theater mode
- Liquid Glass UI — Aerolab-inspired liquid glass buttons with SVG distortion filter, specular lighting, and frosted tint layers on play controls and language selector
- Glassmorphism UI — macOS-inspired dark theme with frosted glass surfaces
- 5-band parametric equalizer — Low (60Hz), Lo-Mid (230Hz), Mid (910Hz), Hi-Mid (3.6kHz), High (14kHz)
- 8 built-in presets — Flat, Bass Boost, Treble, V-Shape, Vocal, Rock, Electronic, Acoustic
- Custom presets — save and name your own EQ configurations
- Persistent EQ preset — selected preset name persists across sessions
- Seamless effects toggle — enable/disable audio enhancements without stream interruption (always-proxy architecture)
- Volume control with mute toggle
- Synced lyrics (LRC format) with auto-scrolling and highlighted current line
- Plain text lyrics fallback when synced version unavailable
- Realtime STT lyrics sync — on-device speech recognition aligns lyrics in real time when timestamps are unavailable
- Local caching for instant re-display on revisit
- Powered by LrcLib API
- Multi-language UI — locale-aware routing via
[countryCode]segments - Language selector — switch UI language in-app
- Country-based defaults — auto-detects preferred stations and language from locale
- Favorites — star stations for quick access
- Favorite songs — bookmark tracks you love
- Recent stations — last 15 played stations
- Play history — up to 100 entries with artist, track, and artwork
- Artist detail modal — view artist biography, upcoming Bandsintown concerts, and lyrics from history/favorites
- Social share — share current station or track via Web Share API (or clipboard fallback) from cards, detail modals, theater mode, and the now-playing bar
- Uniform card layout — all cards share the same height with station name anchored at the bottom
- Group by artist/album — horizontal scrollable thumbnail row for grouped tracks
- Adapts from desktop (sidebar + main) to tablet and mobile layouts
- Touch-friendly controls throughout
- Mobile lyrics reel — swipeable lyrics experience on small screens
- Single-row genre/country chips — horizontally scrollable with a dropdown to access all genres and countries
- Desktop stats modal — floating centered dialog instead of bottom sheet on desktop viewports
- Dev API console — in-browser scrollable log of all ICY, iTunes, Lyrics, and Bandsintown API requests (development mode only, visible in theater mode)
- Node.js 18.17 or later
- npm, yarn, pnpm, or bun
# Clone the repository
git clone https://github.com/CMolG/pulse-radio.git
cd pulse-radio
# Install dependencies
npm install
# Start the development server
npm run devOpen http://localhost:3000 to start listening.
npm run build
npm startcp .env.example .env.local| Variable | Required | Description |
|---|---|---|
CRON_SECRET |
Yes (prod) | Secures /api/cron/sync endpoint. Generate with openssl rand -hex 32 |
BANDSINTOWN_APP_ID |
No | Bandsintown API key for concert data. Falls back to demo key |
NODE_ENV |
Auto | Set by Next.js (development / production / test) |
src/
├── app/ # Next.js App Router
│ ├── [countryCode]/ # Locale-aware routing
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ ├── head.tsx
│ │ └── not-found.tsx
│ ├── api/
│ │ ├── proxy-stream/ # CORS proxy for audio streams
│ │ ├── icy-meta/ # ICY metadata extraction endpoint
│ │ ├── itunes/ # Album artwork lookup proxy
│ │ ├── artist-info/ # Artist biography/info proxy
│ │ ├── concerts/ # Bandsintown concert data proxy
│ │ ├── lyrics/ # Lyrics fetching endpoint
│ │ ├── health/ # Health check endpoint
│ │ ├── analytics/ # Analytics endpoint
│ │ ├── cron/ # Scheduled sync jobs
│ │ └── station-health/ # Station reliability scoring
│ ├── layout.tsx # Root layout (fonts, metadata)
│ ├── page.tsx # Home page → <Radio />
│ ├── sitemap.ts # Dynamic sitemap generation
│ ├── ServiceWorkerRegistrar.tsx
│ └── globals.css # Tailwind + theme variables
│
├── components/radio/ # Main radio player
│ ├── RadioShell.tsx # Top-level container & layout
│ ├── components/ # UI subcomponents
│ │ ├── StationCard.tsx # Station list item
│ │ ├── NowPlayingBar.tsx # Bottom playback bar
│ │ ├── NowPlayingHero.tsx # Main "now playing" display
│ │ ├── BrowseView.tsx # Genre/country browser
│ │ ├── EqPanel.tsx # Equalizer interface
│ │ ├── LyricsPanel.tsx # Lyrics display
│ │ ├── MobileLyricsReel.tsx # Mobile lyrics experience
│ │ ├── LanguageSelector.tsx # UI language switcher
│ │ ├── KeyboardShortcutsHelp.tsx
│ │ ├── Sidebar.tsx # Navigation sidebar
│ │ └── TheaterView.tsx # Full-screen theater mode
│ ├── hooks/ # Custom React hooks
│ │ ├── useRadio.ts # Core playback engine
│ │ ├── useEqualizer.ts # Web Audio API EQ chain
│ │ ├── useStationMeta.ts # ICY metadata polling
│ │ ├── useLyrics.ts # Lyrics fetching & caching
│ │ ├── useRealtimeLyricsSync.ts # STT-based realtime lyrics sync
│ │ ├── useFavorites.ts # Favorite stations (localStorage)
│ │ ├── useFavoriteSongs.ts # Favorite songs (localStorage)
│ │ ├── useRecent.ts # Recent stations
│ │ ├── useHistory.ts # Play history tracker
│ │ ├── useMediaSession.ts # Browser media session
│ │ ├── useStationQueue.ts # Station queue management
│ │ ├── useSleepTimer.ts # Auto-stop sleep timer
│ │ ├── useWakeLock.ts # Screen wake lock
│ │ ├── usePlaybackPosition.ts
│ │ ├── useArtistInfo.ts # Artist biography lookup
│ │ ├── useAudioReactiveBackground.ts
│ │ └── useParallaxBg.ts
│ ├── services/ # External API clients
│ │ ├── radioApi.ts # Radio Browser API
│ │ ├── lyricsApi.ts # LrcLib API
│ │ ├── lyricsAligner.ts # Incremental lyrics alignment
│ │ ├── realtimeSpeechRecognition.ts # STT engine wrapper
│ │ ├── realtimeLyricsTypes.ts
│ │ ├── archiveApi.ts # Internet Archive API
│ │ ├── librivoxApi.ts # LibriVox API
│ │ └── podcastApi.ts # Podcast RSS parsing
│ ├── types.ts # TypeScript type definitions
│ ├── constants.ts # Genres, EQ presets, storage keys
│ ├── lrcParser.ts # LRC lyrics format parser
│ └── lyricsUtils.ts # Lyrics utility functions
│
├── context/
│ └── LocaleContext.tsx # i18n locale provider
│
└── lib/ # Shared utilities
├── audio-visualizer/ # Canvas-based visualizations
│ ├── useAudioAnalyser.ts # FFT frequency analysis hook
│ ├── useAlbumArt.ts # iTunes artwork with Jaro-distance matching
│ ├── FerrofluidRenderer.tsx
│ ├── SpiralRenderer.tsx
│ └── CircularRenderer.tsx
├── i18n/ # Internationalization
│ ├── locales.ts # Supported locales
│ ├── messages.ts # Translation strings
│ ├── countries.ts # Country metadata
│ ├── countryChips.ts # Country filter chips
│ ├── countryDefaults.ts # Per-country defaults
│ ├── languageMap.ts # Language → locale mapping
│ └── localeStorage.ts # Locale persistence
├── playbackStore.ts # Zustand global state
└── storageUtils.ts # localStorage helpers
| Layer | Technology |
|---|---|
| Framework | Next.js 16 with React 19 & React Compiler |
| Language | TypeScript 5 (strict mode) |
| Styling | Tailwind CSS 4 |
| State | Zustand 5 |
| Animation | Motion (Framer Motion) 12 |
| Audio | Web Audio API (biquad filters, FFT analysis) |
| Speech | Web Speech API (SpeechRecognition) |
| Icons | Lucide React |
| API | Purpose |
|---|---|
| Radio Browser | Station discovery (40K+ stations) |
| LrcLib | Synced & plain text lyrics |
| iTunes Search | Album artwork lookup |
| Bandsintown | Upcoming concert/tour dates |
We welcome contributions of all kinds! See CONTRIBUTING.md for the full guide covering setup, testing, code style, and architecture.
- Fork the repository
- Create a feature branch:
git checkout -b feat/my-feature - Make your changes
- Test locally:
npm run build && npx playwright test --project=mobile-chrome - Lint:
npm run lint - Commit with a descriptive message:
git commit -m "feat: add sleep timer" - Push to your fork:
git push origin feat/my-feature - Open a Pull Request
We use Conventional Commits:
feat: New feature
fix: Bug fix
docs: Documentation only
style: Formatting, no code change
refactor: Code change that neither fixes a bug nor adds a feature
perf: Performance improvement
test: Adding or updating tests
chore: Build process or tooling changes
Look for issues labeled good first issue — these are beginner-friendly tasks that are a great starting point.
- The CORS proxy (
/api/proxy-stream) is required because browsers block cross-origin audio streams for Web Audio API analysis - ICY metadata is extracted server-side since browsers can't read ICY headers directly
- The equalizer uses a chain of
BiquadFilterNodes in a Web Audio graph - Lyrics are cached in
localStorageto avoid redundant API calls - The realtime STT engine resets its restart counter on every successful recognition result;
MAX_RESTARTSmeans consecutive failures, not total restarts
- Sleep timer
- Keyboard shortcuts
- i18n / localization
- Realtime STT lyrics sync
- Station queue
- Bandsintown concert integration (theater mode ticker + artist detail modal)
- Audio-reactive background (with analyser fallback)
- Persistent EQ preset selection
- Always-proxy audio pipeline (no stream interruptions)
- Liquid Glass UI (Aerolab-style buttons)
- Social share (Web Share API + clipboard fallback)
- Dev API console (development mode)
- Podcast support (search & RSS playback)
- Audiobook support (LibriVox + Internet Archive)
- Station search with fuzzy matching
- Chromecast / AirPlay support
- Shared playlists
This project is licensed under the Apache License 2.0.
Built with ❤️ and way too many radio stations