An online radio player streaming lossless FLAC audio. Built for local prototyping with Express + SQLite โ no bundler, no framework, just vanilla JS.
- Lossless HLS playback via hls.js, locked to the FLAC rendition of the stream
- Live now-playing โ artist, title, album art, source/stream quality, polled every 20 s
- Per-track ratings (๐ / ๐) โ one vote per browser identity, changeable; voter ID derived server-side from IP + User-Agent hash so no login is required
- Previously played โ last 5 tracks from the metadata feed
- Stations CRUD โ add and remove stream stations, persisted in SQLite
- Dark / light mode โ class-based toggle with system-preference detection and no flash on load
| Layer | Technology |
|---|---|
| Server | Node.js โฅ 22.5, Express 4 |
| Database | SQLite via built-in node:sqlite (no native compilation) |
| Styles | Tailwind CSS v4 CLI, Radio Shack brand theme |
| Frontend | Vanilla JS ES modules (no bundler), hls.js via CDN |
| Testing | Vitest โ node environment for server, jsdom for UI |
Prerequisites: Node.js โฅ 22.5. Use nvm:
nvm use # picks up .nvmrc โ lts/*Install and run:
npm install
npm run dev # Tailwind watch + nodemon in one terminalThen open http://localhost:3000.
npm start is the production-style command โ it builds CSS once then starts the server without file watching.
| Script | What it does |
|---|---|
npm run dev |
Tailwind watch + nodemon (concurrent, prefixed output) |
npm run dev:docker |
Same but uses nodemon-driven CSS builds โ used by the Docker dev profile |
npm start |
One-shot CSS build then node server.js |
npm run css:build |
Compile src/input.css โ public/style.css (minified) |
npm run css:watch |
Tailwind watch only |
npm run db:init |
Reset DB schema and re-insert seed stations |
npm test |
Run full test suite once |
npm run test:watch |
Vitest in watch mode |
npm run test:coverage |
Coverage report via v8 |
Two profiles are provided via docker-compose.yml:
Dev โ bind-mounts the repo for live hot-reload. nodemon restarts the server on JS changes; CSS rebuilds when src/ or public/ files change.
docker compose --profile dev upProd โ lean ~57 MB image (node:24-alpine, production deps only). SQLite data is persisted in a named Docker volume (db_data) so it survives container restarts and image rebuilds.
# Optional: set a secret salt for voter-ID hashing (defaults to a placeholder)
export VOTER_SALT=your-random-secret
docker compose --profile prod up --buildThen open http://localhost:3000.
server.js Express app โ exports createApp(db) and getVoterId for testing
db/
init.js Schema definitions, seed data, openDb() helper
data.db SQLite file (gitignored, created on first run)
src/
input.css Tailwind source โ @theme tokens, dark mode variant, .brand-input
public/
index.html Single-page app shell (scripts loaded as type="module")
player.js HLS playback, FLAC level selection, status updates
ratings-ui.js Exported pure functions โ applyBtnState, renderRatings, format helpers
metadata.js Metadata polling, cover art; imports from ratings-ui.js
app.js Stations list CRUD
logo.png Radio Shack logo
style.css Generated by Tailwind (gitignored)
tests/
server.ratings.test.js Backend tests โ getVoterId, GET/POST /api/ratings (node)
ui.ratings.test.js Frontend tests โ button states, render logic, format functions (jsdom)
vitest.config.js Test runner config
RadioShack_Style_Guide.txt Brand colours, typography, component specs
| Method | Path | Description |
|---|---|---|
| GET | /api/health |
Liveness check |
| GET | /api/stations |
List all stations |
| POST | /api/stations |
Create station โ body: { name, stream_url, genre? } |
| DELETE | /api/stations/:id |
Delete station |
| GET | /api/ratings?track_key= |
Get { up, down, user_vote } for a track |
| POST | /api/ratings |
Submit rating โ body: { track_key, rating: 1 | -1 } |
Voter identity is computed server-side as SHA-256(VOTER_SALT | ip | user-agent) โ no client storage required. Set the VOTER_SALT environment variable in production. Behind a reverse proxy, add app.set('trust proxy', 1) to server.js so req.ip resolves correctly.
The default stream is a lossless FLAC HLS feed. Its master playlist exposes two renditions:
| Rendition | Codec | Quality |
|---|---|---|
flac_hires.m3u8 |
fLaC |
16-bit / 44.1 kHz lossless โ player pins here |
aac_hifi.m3u8 |
mp4a.40.2 |
AAC HiFi |
Now-playing metadata and cover art are fetched directly from the same CDN origin (metadatav2.json, cover.jpg). Both endpoints send Access-Control-Allow-Origin: *.
Colours, typography, button styles, and spacing rules are documented in RadioShack_Style_Guide.txt. The Tailwind theme tokens in src/input.css map directly to that palette.