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:
parent
13a2ad52e5
commit
8170cfb7a2
141
README.md
141
README.md
|
|
@ -1,36 +1,56 @@
|
||||||
# Tunes
|
# Tunes
|
||||||
|
|
||||||
A self-contained music website: browse and listen to songs and playlists,
|
A self-hosted music website: browse and listen to songs and playlists, with
|
||||||
with user accounts and an admin upload UI. Everything runs from a single
|
user accounts, social features, and AI-assisted music and cover art generation.
|
||||||
Node.js process with an embedded SQLite database and local media storage —
|
Everything runs from a single Node.js process with an embedded SQLite database
|
||||||
no external services required.
|
and local media storage — no external services required beyond an optional
|
||||||
|
ComfyUI instance for AI generation.
|
||||||
|
|
||||||
## Features (v1)
|
## Features
|
||||||
|
|
||||||
- **Public browse & listen**
|
- **Public browse & listen**
|
||||||
- Home page with featured playlists and recently added songs
|
- Home page with featured playlists and recently added songs
|
||||||
- Paginated, searchable song list (title / artist / album)
|
- Paginated, searchable song list (title / artist / album)
|
||||||
- Playlist grid and detail view with "Play all"
|
- Playlist grid and detail view with "Play all"
|
||||||
- Persistent bottom-of-page audio player with queue support
|
- Persistent bottom-of-page audio player with queue support, repeat, and visualizer
|
||||||
- HTTP Range streaming, so seeking/scrubbing works correctly
|
- 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**
|
- **User accounts**
|
||||||
- Email + password registration (argon2id hashing)
|
- Email + password registration (argon2id hashing)
|
||||||
|
- Optional email verification via SMTP
|
||||||
- Login / logout with SQLite-backed sessions (survive restarts)
|
- 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
|
- 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
|
- Duration auto-extracted from the audio file
|
||||||
- Create and edit playlists, add/remove tracks
|
- Create and edit playlists, add/remove/reorder tracks
|
||||||
- Toggle songs and playlists between public and private
|
- Bulk visibility changes
|
||||||
- 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
|
|
||||||
|
|
||||||
## Tech stack
|
## Tech stack
|
||||||
|
|
||||||
|
|
@ -39,10 +59,13 @@ no external services required.
|
||||||
- **EJS** + `express-ejs-layouts` — server-rendered views
|
- **EJS** + `express-ejs-layouts` — server-rendered views
|
||||||
- **argon2** — password hashing
|
- **argon2** — password hashing
|
||||||
- **express-session** + `better-sqlite3-session-store` — persistent sessions
|
- **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
|
- **music-metadata** — extract duration from uploaded audio
|
||||||
|
- **nodemailer** — email verification
|
||||||
- **csrf-sync**, **zod**, **pino** — CSRF, validation, logging
|
- **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
|
## Getting started
|
||||||
|
|
||||||
|
|
@ -50,6 +73,7 @@ no external services required.
|
||||||
|
|
||||||
- Node.js 18+ (tested on 18.19)
|
- Node.js 18+ (tested on 18.19)
|
||||||
- npm
|
- npm
|
||||||
|
- *(optional)* A running [ComfyUI](https://github.com/comfyanonymous/ComfyUI) instance for AI generation
|
||||||
|
|
||||||
### Install and run
|
### Install and run
|
||||||
|
|
||||||
|
|
@ -58,13 +82,11 @@ git clone <your-repo-url> tunes
|
||||||
cd tunes
|
cd tunes
|
||||||
npm install
|
npm install
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
# Edit .env:
|
# Edit .env — at minimum set SESSION_SECRET and ADMIN_BOOTSTRAP_EMAIL
|
||||||
# SESSION_SECRET=<a long random string>
|
|
||||||
# ADMIN_BOOTSTRAP_EMAIL=<your email>
|
|
||||||
node server.js
|
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.
|
automatically against `tunes.db` in the project root.
|
||||||
|
|
||||||
For auto-reload during development:
|
For auto-reload during development:
|
||||||
|
|
@ -78,35 +100,51 @@ npm run dev
|
||||||
1. Open http://localhost:3000/register
|
1. Open http://localhost:3000/register
|
||||||
2. Register with the email you set as `ADMIN_BOOTSTRAP_EMAIL` in `.env`.
|
2. Register with the email you set as `ADMIN_BOOTSTRAP_EMAIL` in `.env`.
|
||||||
That first matching registration is automatically given the `admin` role.
|
That first matching registration is automatically given the `admin` role.
|
||||||
3. Once logged in, visit `/admin/songs` to upload audio and `/admin/playlists`
|
3. Visit `/admin` to manage users and configure generation settings.
|
||||||
to create playlists.
|
4. Visit `/mymusic/songs` to upload audio and `/mymusic/playlists` to create playlists.
|
||||||
|
|
||||||
Any subsequent registrations are normal users.
|
Any subsequent registrations are normal users.
|
||||||
|
|
||||||
### Adding music
|
### Adding music
|
||||||
|
|
||||||
1. Go to `/admin/songs/new`
|
1. Go to `/mymusic/songs/new`
|
||||||
2. Fill in title, artist, and (optionally) album/genre/year
|
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)
|
3. Choose an audio file (mp3, m4a, ogg, flac, wav, opus, webm — up to 100 MB)
|
||||||
4. Optionally add a cover image
|
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
|
Uploaded files live under `media/audio/` and `media/covers/`. The database
|
||||||
stores relative paths, so you can move the whole project folder freely as
|
stores relative paths, so you can move the whole project folder freely as long
|
||||||
long as `media/` and `tunes.db` stay together.
|
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
|
## Configuration
|
||||||
|
|
||||||
All settings come from environment variables (loaded from `.env` via
|
All settings come from environment variables (loaded from `.env` via `dotenv`):
|
||||||
`dotenv`):
|
|
||||||
|
|
||||||
| Variable | Default | Purpose |
|
| Variable | Default | Purpose |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `PORT` | `3000` | HTTP port |
|
| `PORT` | `3000` | HTTP port |
|
||||||
| `SESSION_SECRET` | *(insecure default)* | Session cookie signing secret — **set this in production** |
|
| `SESSION_SECRET` | *(insecure default)* | Session cookie signing key — **set this in production** |
|
||||||
| `ADMIN_BOOTSTRAP_EMAIL` | *(unset)* | Email that auto-becomes admin on first registration |
|
| `ADMIN_BOOTSTRAP_EMAIL` | *(unset)* | Email auto-promoted to admin on first registration |
|
||||||
| `DB_PATH` | `./tunes.db` | SQLite database file |
|
| `DB_PATH` | `./tunes.db` | SQLite database file path |
|
||||||
| `MEDIA_DIR` | `./media` | Root directory for audio and cover uploads |
|
| `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
|
## Project layout
|
||||||
|
|
||||||
|
|
@ -115,26 +153,31 @@ tunes/
|
||||||
├── server.js # entry point: loads env, runs migrations, starts app
|
├── server.js # entry point: loads env, runs migrations, starts app
|
||||||
├── db/
|
├── db/
|
||||||
│ ├── index.js # SQLite connection + migration runner
|
│ ├── index.js # SQLite connection + migration runner
|
||||||
│ └── migrations/*.sql # schema migrations
|
│ └── migrations/*.sql # numbered schema migrations
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── app.js # Express wiring (sessions, CSRF, views, routes)
|
│ ├── app.js # Express wiring (sessions, CSRF, views, routes)
|
||||||
│ ├── middleware/auth.js # requireUser / requireAdmin
|
│ ├── middleware/auth.js # requireUser / requireAdmin / requireVip
|
||||||
│ ├── routes/
|
│ ├── routes/
|
||||||
│ │ ├── public.js # /, /songs, /playlists, /stream/:id
|
│ │ ├── public.js # /, /songs, /playlists, /profiles, /stream/:id
|
||||||
│ │ ├── auth.js # /register, /login, /logout
|
│ │ ├── auth.js # /register, /login, /logout, /verify-email
|
||||||
│ │ ├── account.js # /account
|
│ │ ├── account.js # /account — profile and password settings
|
||||||
│ │ └── admin.js # /admin/songs, /admin/playlists
|
│ │ ├── admin.js # /mymusic — user song and playlist management
|
||||||
│ ├── services/ # users, songs, playlists (DB access)
|
│ │ ├── 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
|
│ └── views/ # EJS templates
|
||||||
├── public/ # static CSS, JS, vendored htmx
|
├── public/
|
||||||
└── media/ # runtime audio + cover uploads (git-ignored)
|
│ ├── css/app.css # main stylesheet
|
||||||
|
│ └── js/ # player, visualizer, social, lightbox, etc.
|
||||||
|
└── media/ # runtime uploads and generated files (git-ignored)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Data and backups
|
## Data and backups
|
||||||
|
|
||||||
Everything you care about lives in two places:
|
Everything you care about lives in two places:
|
||||||
|
|
||||||
- `tunes.db` — users, songs, playlists, sessions
|
- `tunes.db` — users, songs, playlists, sessions, notifications, generation records
|
||||||
- `media/` — audio files and cover art
|
- `media/` — audio files, cover art, avatars, and AI-generated files
|
||||||
|
|
||||||
Back up both together to preserve the full state of the site.
|
Back up both together to preserve the full state of the site.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "tunes",
|
"name": "tunes",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Self-contained music website with browse/listen + user accounts",
|
"description": "Self-contained music website with browse/listen + user accounts",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,7 @@
|
||||||
const cur = state.queue[state.index];
|
const cur = state.queue[state.index];
|
||||||
if (!cur) return;
|
if (!cur) return;
|
||||||
window.briTunesNowPlaying = cur;
|
window.briTunesNowPlaying = cur;
|
||||||
|
transitioning = true;
|
||||||
audio.src = `/stream/${cur.id}`;
|
audio.src = `/stream/${cur.id}`;
|
||||||
audio.play()
|
audio.play()
|
||||||
.then(() => window.dispatchEvent(new CustomEvent('briTunes:play', { detail: cur })))
|
.then(() => window.dispatchEvent(new CustomEvent('briTunes:play', { detail: cur })))
|
||||||
|
|
@ -128,9 +129,30 @@
|
||||||
if (state.index > 0) { state.index -= 1; playCurrent(); }
|
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('ended', next);
|
||||||
audio.addEventListener('play', () => { if ('mediaSession' in navigator) navigator.mediaSession.playbackState = 'playing'; });
|
audio.addEventListener('play', () => {
|
||||||
audio.addEventListener('pause', () => { if ('mediaSession' in navigator) navigator.mediaSession.playbackState = 'paused'; });
|
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);
|
nextBtn.addEventListener('click', next);
|
||||||
|
|
||||||
if ('mediaSession' in navigator) {
|
if ('mediaSession' in navigator) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const CACHE = 'bri-tunes-v2';
|
const CACHE = 'bri-tunes-v3';
|
||||||
const STATIC_PRECACHE = [
|
const STATIC_PRECACHE = [
|
||||||
'/',
|
'/',
|
||||||
'/static/css/app.css',
|
'/static/css/app.css',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue