aajolly/radio-shack

Build an online radio station, ready for production!

โ˜… 0Forks 0JavaScriptGitHub โ†—Compare

README

Radio Shack ๐Ÿ“ป

An online radio player streaming lossless FLAC audio. Built for local prototyping with Express + SQLite โ€” no bundler, no framework, just vanilla JS.


Features

  • 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

Stack

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

Getting Started

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 terminal

Then open http://localhost:3000.

npm start is the production-style command โ€” it builds CSS once then starts the server without file watching.

Scripts

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

Docker

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 up

Prod โ€” 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 --build

Then open http://localhost:3000.

Project Structure

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

API

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.

Stream

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: *.

Brand

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.

Contributors

aajollygithub-actions[bot]

Issues