- Add genre and uploader filters to /songs with persistent query params in pagination - Implement site-wide "Trending" strip showing recently liked songs - Introduce responsive hamburger nav for mobile with animated icon & ARIA support - Refactor song row layout: move duration into separate `.song-duration` element - Update CSS for mobile grid (2 columns), optimized song rows, and facet selects |
||
|---|---|---|
| db | ||
| public | ||
| src | ||
| .env.example | ||
| .gitignore | ||
| PWA.md | ||
| README.md | ||
| package-lock.json | ||
| package.json | ||
| server.js | ||
README.md
Tunes
A self-contained music website: browse and listen to songs and playlists, with user accounts and an admin upload UI. Everything runs from a single Node.js process with an embedded SQLite database and local media storage — no external services required.
Features (v1)
- Public browse & listen
- Home page with featured playlists and recently added songs
- Paginated, searchable song list (title / artist / album)
- Playlist grid and detail view with "Play all"
- Persistent bottom-of-page audio player with queue support
- HTTP Range streaming, so seeking/scrubbing works correctly
- User accounts
- Email + password registration (argon2id hashing)
- Login / logout with SQLite-backed sessions (survive restarts)
- Account page: update display name, change password
- CSRF protection on all state-changing routes
- Admin
- Upload songs with cover art; title/artist/album/genre/year metadata
- Duration auto-extracted from the audio file
- Create and edit playlists, add/remove tracks
- Toggle songs and playlists between public and private
- First user who registers with
ADMIN_BOOTSTRAP_EMAILis auto-promoted to admin (only while no admin exists yet)
Roadmap (not in v1)
- Generate new music via an external API from within the app
- Let logged-in users publish their own songs and create playlists
- Tune into streaming radio stations
Tech stack
- Node.js + Express 4 — server and routing
- SQLite via
better-sqlite3— embedded database, WAL mode - EJS +
express-ejs-layouts— server-rendered views - argon2 — password hashing
- express-session +
better-sqlite3-session-store— persistent sessions - multer — file uploads (audio + cover art)
- music-metadata — extract duration from uploaded audio
- csrf-sync, zod, pino — CSRF, validation, logging
- htmx (vendored) — small sprinklings of interactivity, no build step
Getting started
Prerequisites
- Node.js 18+ (tested on 18.19)
- npm
Install and run
git clone <your-repo-url> tunes
cd tunes
npm install
cp .env.example .env
# Edit .env:
# SESSION_SECRET=<a long random string>
# ADMIN_BOOTSTRAP_EMAIL=<your email>
node server.js
The server boots at http://localhost:3000 and runs database migrations
automatically against tunes.db in the project root.
For auto-reload during development:
npm run dev
Creating the first admin
- Open http://localhost:3000/register
- Register with the email you set as
ADMIN_BOOTSTRAP_EMAILin.env. That first matching registration is automatically given theadminrole. - Once logged in, visit
/admin/songsto upload audio and/admin/playliststo create playlists.
Any subsequent registrations are normal users.
Adding music
- Go to
/admin/songs/new - Fill in title, artist, and (optionally) album/genre/year
- Choose an audio file (mp3, m4a, ogg, flac, wav, opus, webm — up to 100 MB)
- Optionally add a cover image
- Check "Public" so visitors can see and play it, then click Upload
Uploaded files live under media/audio/ and media/covers/. The database
stores relative paths, so you can move the whole project folder freely as
long as media/ and tunes.db stay together.
Configuration
All settings come from environment variables (loaded from .env via
dotenv):
| Variable | Default | Purpose |
|---|---|---|
PORT |
3000 |
HTTP port |
SESSION_SECRET |
(insecure default) | Session cookie signing secret — set this in production |
ADMIN_BOOTSTRAP_EMAIL |
(unset) | Email that auto-becomes admin on first registration |
DB_PATH |
./tunes.db |
SQLite database file |
MEDIA_DIR |
./media |
Root directory for audio and cover uploads |
Project layout
tunes/
├── server.js # entry point: loads env, runs migrations, starts app
├── db/
│ ├── index.js # SQLite connection + migration runner
│ └── migrations/*.sql # schema migrations
├── src/
│ ├── app.js # Express wiring (sessions, CSRF, views, routes)
│ ├── middleware/auth.js # requireUser / requireAdmin
│ ├── routes/
│ │ ├── public.js # /, /songs, /playlists, /stream/:id
│ │ ├── auth.js # /register, /login, /logout
│ │ ├── account.js # /account
│ │ └── admin.js # /admin/songs, /admin/playlists
│ ├── services/ # users, songs, playlists (DB access)
│ └── views/ # EJS templates
├── public/ # static CSS, JS, vendored htmx
└── media/ # runtime audio + cover uploads (git-ignored)
Data and backups
Everything you care about lives in two places:
tunes.db— users, songs, playlists, sessionsmedia/— audio files and cover art
Back up both together to preserve the full state of the site.