feat: add social features, user uploads, AI music generation, and PWA enhancements

- Add social features: like/favorite songs and playlists, notifications, and public profile pages
- Enable user music: logged-in users can upload songs and create playlists with visibility controls and NSFW flag
- Implement AI music generation via ComfyUI with per-user rate limiting, workflow configuration, and optional cover art generation
- Expand admin panel for user management and AI settings
- Enhance PWA support: lock-screen controls, background playback, and visualizer
- Add email verification via SMTP and avatar cropping
- Update package version to 1.0.0 and bump service worker cache
This commit is contained in:
Brian Fertig 2026-04-17 20:12:34 -06:00
parent 13a2ad52e5
commit 8170cfb7a2
4 changed files with 118 additions and 53 deletions

141
README.md
View File

@ -1,36 +1,56 @@
# 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.
A self-hosted music website: browse and listen to songs and playlists, with
user accounts, social features, and AI-assisted music and cover art generation.
Everything runs from a single Node.js process with an embedded SQLite database
and local media storage — no external services required beyond an optional
ComfyUI instance for AI generation.
## Features (v1)
## Features
- **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
- Persistent bottom-of-page audio player with queue support, repeat, and visualizer
- HTTP Range streaming so seeking/scrubbing works correctly
- PWA-ready with Media Session API: lock-screen controls and background playback on mobile
- Lightbox for cover art
- **User accounts**
- Email + password registration (argon2id hashing)
- Optional email verification via SMTP
- Login / logout with SQLite-backed sessions (survive restarts)
- Account page: update display name, change password
- Account page: update display name, avatar, and password
- Public profile pages with song/playlist activity
- CSRF protection on all state-changing routes
- **Admin**
- Upload songs with cover art; title/artist/album/genre/year metadata
- **Social**
- Like and favorite songs and playlists
- In-app notifications
- Profile pages showing liked and favorited content
- **User music**
- Logged-in users can upload their own songs and create playlists
- Visibility controls: `public`, `logged_in`, `vip`, or `private`
- NSFW flag for explicit content
- **AI music generation** (requires ComfyUI)
- Generate songs from style prompts, lyrics, BPM, key, duration, and creativity settings
- Per-user rate limiting (cooldown + hourly cap)
- Generated songs can be previewed, published, or deleted
- Optionally generate album cover art after publishing
- **Admin panel** (`/admin`)
- User management: disable, enable, verify, grant/revoke VIP, delete
- ComfyUI integration settings: base URL, music workflow JSON, image workflow JSON, node ID mappings
- Toggle music generation and image generation on/off site-wide
- **My Music** (`/mymusic`) — for any logged-in user
- Upload songs with cover art; title/artist/album/genre/year/BPM 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_EMAIL` is 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
- Create and edit playlists, add/remove/reorder tracks
- Bulk visibility changes
## Tech stack
@ -39,10 +59,13 @@ no external services required.
- **EJS** + `express-ejs-layouts` — server-rendered views
- **argon2** — password hashing
- **express-session** + `better-sqlite3-session-store` — persistent sessions
- **multer** — file uploads (audio + cover art)
- **multer** — file uploads (audio, cover art, avatars)
- **sharp** — server-side image resizing and conversion
- **music-metadata** — extract duration from uploaded audio
- **nodemailer** — email verification
- **csrf-sync**, **zod**, **pino** — CSRF, validation, logging
- **htmx** (vendored) — small sprinklings of interactivity, no build step
- **htmx** (vendored) — progressive interactivity, no build step
- **CropperJS** (vendored) — client-side avatar cropping
## Getting started
@ -50,6 +73,7 @@ no external services required.
- Node.js 18+ (tested on 18.19)
- npm
- *(optional)* A running [ComfyUI](https://github.com/comfyanonymous/ComfyUI) instance for AI generation
### Install and run
@ -58,13 +82,11 @@ 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>
# Edit .env — at minimum set SESSION_SECRET and ADMIN_BOOTSTRAP_EMAIL
node server.js
```
The server boots at http://localhost:3000 and runs database migrations
The server starts at http://localhost:3000 and runs database migrations
automatically against `tunes.db` in the project root.
For auto-reload during development:
@ -78,35 +100,51 @@ npm run dev
1. Open http://localhost:3000/register
2. Register with the email you set as `ADMIN_BOOTSTRAP_EMAIL` in `.env`.
That first matching registration is automatically given the `admin` role.
3. Once logged in, visit `/admin/songs` to upload audio and `/admin/playlists`
to create playlists.
3. Visit `/admin` to manage users and configure generation settings.
4. Visit `/mymusic/songs` to upload audio and `/mymusic/playlists` to create playlists.
Any subsequent registrations are normal users.
### Adding music
1. Go to `/admin/songs/new`
1. Go to `/mymusic/songs/new`
2. Fill in title, artist, and (optionally) album/genre/year
3. Choose an audio file (mp3, m4a, ogg, flac, wav, opus, webm — up to 100 MB)
4. Optionally add a cover image
5. Check "Public" so visitors can see and play it, then click **Upload**
5. Set visibility to **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.
stores relative paths, so you can move the whole project folder freely as long
as `media/` and `tunes.db` stay together.
### AI generation setup
1. Stand up a ComfyUI instance accessible from the server.
2. In `/admin`, set the **ComfyUI Base URL** (e.g. `http://localhost:8188`).
3. Upload a **Music Workflow JSON** and fill in the node ID mappings for your workflow.
4. Optionally upload an **Image Workflow JSON** and set the prompt node ID for cover art.
5. Check **Enable Song Generation** (and **Enable Album Cover Generation** if desired).
6. VIP users (and admins) can now visit `/generate` to produce songs.
## Configuration
All settings come from environment variables (loaded from `.env` via
`dotenv`):
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 |
| `SESSION_SECRET` | *(insecure default)* | Session cookie signing key — **set this in production** |
| `ADMIN_BOOTSTRAP_EMAIL` | *(unset)* | Email auto-promoted to admin on first registration |
| `DB_PATH` | `./tunes.db` | SQLite database file path |
| `MEDIA_DIR` | `./media` | Root directory for all uploaded and generated files |
| `SITE_NAME` | `Bri-Tunes` | Site name shown in the header and page titles |
| `APP_BASE_URL` | `http://localhost:3000` | Full URL used in email verification links |
| `SMTP_HOST` | *(unset)* | SMTP server hostname (leave blank to print verify URLs to console) |
| `SMTP_PORT` | `587` | SMTP port |
| `SMTP_SECURE` | `false` | Use TLS (`true`) or STARTTLS (`false`) |
| `SMTP_USER` | *(unset)* | SMTP username |
| `SMTP_PASS` | *(unset)* | SMTP password |
| `SMTP_FROM` | *(unset)* | `From` address for outgoing email |
## Project layout
@ -115,26 +153,31 @@ tunes/
├── server.js # entry point: loads env, runs migrations, starts app
├── db/
│ ├── index.js # SQLite connection + migration runner
│ └── migrations/*.sql # schema migrations
│ └── migrations/*.sql # numbered schema migrations
├── src/
│ ├── app.js # Express wiring (sessions, CSRF, views, routes)
│ ├── middleware/auth.js # requireUser / requireAdmin
│ ├── middleware/auth.js # requireUser / requireAdmin / requireVip
│ ├── 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)
│ │ ├── public.js # /, /songs, /playlists, /profiles, /stream/:id
│ │ ├── auth.js # /register, /login, /logout, /verify-email
│ │ ├── account.js # /account — profile and password settings
│ │ ├── admin.js # /mymusic — user song and playlist management
│ │ ├── admin-panel.js # /admin — site administration
│ │ ├── generate.js # /generate — AI music + cover art generation
│ │ └── social.js # /api — likes, favorites
│ ├── services/ # users, songs, playlists, social, generation, settings
│ └── views/ # EJS templates
├── public/ # static CSS, JS, vendored htmx
└── media/ # runtime audio + cover uploads (git-ignored)
├── public/
│ ├── css/app.css # main stylesheet
│ └── js/ # player, visualizer, social, lightbox, etc.
└── media/ # runtime uploads and generated files (git-ignored)
```
## Data and backups
Everything you care about lives in two places:
- `tunes.db` — users, songs, playlists, sessions
- `media/` — audio files and cover art
- `tunes.db` — users, songs, playlists, sessions, notifications, generation records
- `media/` — audio files, cover art, avatars, and AI-generated files
Back up both together to preserve the full state of the site.

View File

@ -1,6 +1,6 @@
{
"name": "tunes",
"version": "0.1.0",
"version": "1.0.0",
"private": true,
"description": "Self-contained music website with browse/listen + user accounts",
"main": "server.js",

View File

@ -91,6 +91,7 @@
const cur = state.queue[state.index];
if (!cur) return;
window.briTunesNowPlaying = cur;
transitioning = true;
audio.src = `/stream/${cur.id}`;
audio.play()
.then(() => window.dispatchEvent(new CustomEvent('briTunes:play', { detail: cur })))
@ -128,9 +129,30 @@
if (state.index > 0) { state.index -= 1; playCurrent(); }
}
// Track whether we're in the middle of a track transition so the 'pause'
// event fired by src reassignment doesn't mislead iOS into thinking playback stopped.
let transitioning = false;
audio.addEventListener('ended', next);
audio.addEventListener('play', () => { if ('mediaSession' in navigator) navigator.mediaSession.playbackState = 'playing'; });
audio.addEventListener('pause', () => { if ('mediaSession' in navigator) navigator.mediaSession.playbackState = 'paused'; });
audio.addEventListener('play', () => {
transitioning = false;
if ('mediaSession' in navigator) navigator.mediaSession.playbackState = 'playing';
});
audio.addEventListener('pause', () => {
if (transitioning) return;
if ('mediaSession' in navigator) navigator.mediaSession.playbackState = 'paused';
});
// setPositionState keeps iOS from throttling the JS thread during background playback.
audio.addEventListener('timeupdate', () => {
if (!('mediaSession' in navigator) || !audio.duration || !isFinite(audio.duration)) return;
try {
navigator.mediaSession.setPositionState({
duration: audio.duration,
playbackRate: audio.playbackRate,
position: audio.currentTime,
});
} catch (_) {}
});
nextBtn.addEventListener('click', next);
if ('mediaSession' in navigator) {

View File

@ -1,4 +1,4 @@
const CACHE = 'bri-tunes-v2';
const CACHE = 'bri-tunes-v3';
const STATIC_PRECACHE = [
'/',
'/static/css/app.css',