commit 7bc01a38287703409901b1953a4941f54156bc52 Author: Scott Register Date: Fri Dec 12 12:39:07 2025 -0800 add spec.md diff --git a/spec.md b/spec.md new file mode 100644 index 0000000..be2d6b5 --- /dev/null +++ b/spec.md @@ -0,0 +1,718 @@ +# Discord Music Bot with Web DJ Interface + +## Project Overview + +A self-hosted Discord bot that plays local MP3 files in a voice channel on continuous loop, paired with a web-based DJ interface that allows users to view and control playback. + +--- + +## Goals + +- **Always-on music**: Bot joins a designated voice channel and plays music 24/7 +- **Local library**: All audio files are stored locally on the server (no streaming services) +- **Web control**: Browser-based interface for viewing now-playing and controlling the queue +- **Simple deployment**: Dockerized for easy VPS hosting + +--- + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Docker Host │ +│ ┌───────────────────────────────────────────────────────┐ │ +│ │ Docker Compose │ │ +│ │ │ │ +│ │ ┌─────────────────┐ ┌─────────────────────────┐ │ │ +│ │ │ Bot Service │◄────►│ Web Backend (API) │ │ │ +│ │ │ (Python/JS) │ │ (Node/Python) │ │ │ +│ │ └────────┬────────┘ └───────────┬─────────────┘ │ │ +│ │ │ │ │ │ +│ │ ▼ ▼ │ │ +│ │ ┌─────────────────┐ ┌─────────────────────────┐ │ │ +│ │ │ Discord │ │ Web Frontend │ │ │ +│ │ │ Voice API │ │ (React/Vue/Static) │ │ │ +│ │ └─────────────────┘ └─────────────────────────┘ │ │ +│ │ │ │ +│ │ ┌─────────────────────────────────────────────────┐ │ │ +│ │ │ Shared Volume: /music │ │ │ +│ │ │ (MP3 files) │ │ │ +│ │ └─────────────────────────────────────────────────┘ │ │ +│ └───────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## Components + +### 1. Discord Bot Service + +**Responsibilities:** +- Connect to Discord and join a configured voice channel +- Play MP3 files from local storage +- Loop through playlist continuously +- Accept commands from the web backend via internal API or message queue +- Report current playback state + +**Technology Options:** +- **Python**: `discord.py` with `PyNaCl` for voice +- **Node.js**: `discord.js` with `@discordjs/voice` + +### 2. Web Backend (API Server) + +**Responsibilities:** +- Serve REST API for frontend +- Provide WebSocket connection for real-time updates +- Communicate with bot service to control playback +- Scan and manage the music library + +**Technology Options:** +- **Node.js**: Express or Fastify +- **Python**: FastAPI or Flask + +### 3. Web Frontend + +**Responsibilities:** +- Display now-playing information (track name, progress, album art if available) +- Show upcoming tracks in queue +- Provide DJ controls (skip, pause, reorder, shuffle) +- Playlist management + +**Technology Options:** +- React, Vue, Svelte, or plain HTML/JS +- Tailwind CSS for styling + +### 4. Shared Storage + +- Docker volume mounted to both bot and backend +- Contains all MP3 files +- Optionally: SQLite database for metadata/state + +--- + +## Features + +### MVP (Phase 1) + +| Feature | Description | +|---------|-------------| +| Bot auto-join | Bot joins configured voice channel on startup | +| Continuous playback | Loops through all MP3s in the music directory | +| Now playing display | Web UI shows current track name and artist (from ID3 tags) | +| Skip track | Web UI button to skip to next track | +| Pause/Resume | Web UI controls for pausing and resuming playback | +| Track list | Web UI displays full playlist | +| MP3 upload | Upload MP3 files via web interface | + +### Phase 2 + +| Feature | Description | +|---------|-------------| +| Queue reordering | Drag-and-drop to reorder upcoming tracks | +| Shuffle mode | Toggle shuffle on/off | +| Search | Filter/search tracks in the library | +| Track progress | Show playback progress bar with seek capability | +| Album art | Display embedded album art from MP3 metadata | +| Volume control | Adjust bot playback volume from web UI | + +### Phase 3 (Nice-to-Have) + +| Feature | Description | +|---------|-------------| +| Multi-channel support | Configure multiple channels with separate playlists | +| User authentication | Password-protect the DJ interface | +| Discord slash commands | Control bot via Discord commands (e.g., `/skip`, `/nowplaying`) | +| Listening history | Log of recently played tracks | +| Favorites/playlists | Create and manage multiple playlists | + +--- + +## API Specification + +### REST Endpoints + +#### Library + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/tracks` | List all tracks in library | +| GET | `/api/tracks/:id` | Get track metadata | +| GET | `/api/tracks/:id/art` | Get album art image | +| POST | `/api/tracks/upload` | Upload new MP3 file(s) | +| DELETE | `/api/tracks/:id` | Delete a track from library | +| POST | `/api/tracks/scan` | Rescan music directory for new files | + +#### Playback + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/playback` | Get current playback state | +| POST | `/api/playback/play` | Resume playback | +| POST | `/api/playback/pause` | Pause playback | +| POST | `/api/playback/skip` | Skip to next track | +| POST | `/api/playback/previous` | Go to previous track | +| POST | `/api/playback/seek` | Seek to position (body: `{ position: seconds }`) | +| POST | `/api/playback/volume` | Set volume (body: `{ volume: 0-100 }`) | + +#### Queue + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/queue` | Get current queue | +| POST | `/api/queue/add` | Add track to queue (body: `{ trackId }`) | +| POST | `/api/queue/remove` | Remove track from queue (body: `{ index }`) | +| POST | `/api/queue/reorder` | Reorder queue (body: `{ from, to }`) | +| POST | `/api/queue/shuffle` | Shuffle the queue | +| POST | `/api/queue/clear` | Clear queue (except now playing) | + +#### Bot Status + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/status` | Bot connection status, channel info | + +### WebSocket Events + +**Server → Client:** +```json +{ "event": "trackChange", "data": { "track": {...}, "queue": [...] } } +{ "event": "playbackUpdate", "data": { "state": "playing", "position": 45.2 } } +{ "event": "queueUpdate", "data": { "queue": [...] } } +{ "event": "volumeChange", "data": { "volume": 75 } } +{ "event": "libraryUpdate", "data": { "action": "added", "tracks": [...] } } +{ "event": "libraryUpdate", "data": { "action": "removed", "trackIds": [...] } } +{ "event": "uploadProgress", "data": { "filename": "song.mp3", "progress": 45 } } +``` + +**Client → Server:** +```json +{ "event": "subscribe" } +``` + +--- + +## File Upload Specification + +### Upload Endpoint Details + +**POST `/api/tracks/upload`** + +Accepts multipart/form-data with one or more MP3 files. + +**Request:** +``` +Content-Type: multipart/form-data + +file: +-- or for multiple files -- +files[]: +files[]: +``` + +**Response (Success):** +```json +{ + "success": true, + "uploaded": [ + { + "id": "abc123", + "filename": "song.mp3", + "title": "Song Title", + "artist": "Artist Name", + "duration": 215 + } + ], + "errors": [] +} +``` + +**Response (Partial/Failure):** +```json +{ + "success": false, + "uploaded": [], + "errors": [ + { + "filename": "badfile.txt", + "error": "Invalid file type. Only MP3 files are accepted." + }, + { + "filename": "toobig.mp3", + "error": "File exceeds maximum size of 50MB." + } + ] +} +``` + +### Upload Validation Rules + +| Rule | Constraint | +|------|------------| +| File type | Must be `audio/mpeg` MIME type or `.mp3` extension | +| File size | Maximum 50MB per file (configurable) | +| Filename | Sanitized to remove special characters | +| Duplicates | Option to skip, overwrite, or rename duplicates | +| Batch limit | Maximum 20 files per upload request | + +### Upload Processing Pipeline + +``` +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ Receive │────►│ Validate │────►│ Extract │────►│ Save │ +│ File │ │ Type/Size │ │ Metadata │ │ to Disk │ +└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ + │ + ▼ +┌──────────────┐ ┌──────────────┐ ┌──────────────────────────────────┐ +│ Notify │◄────│ Index │◄────│ Generate ID & Store in DB │ +│ Bot │ │ Library │ │ │ +└──────────────┘ └──────────────┘ └──────────────────────────────────┘ +``` + +### Metadata Extraction + +On upload, the following ID3 tags are extracted (if present): +- Title (falls back to filename) +- Artist +- Album +- Duration +- Album art (stored separately for serving) +- Track number +- Year +- Genre + +**Recommended library:** `music-metadata` (Node.js) or `mutagen` (Python) + +### Frontend Upload Component + +The web UI should include: + +| Feature | Description | +|---------|-------------| +| Drag & drop zone | Drop MP3 files directly onto the page | +| File browser | Traditional file picker button | +| Progress indicator | Upload progress bar for each file | +| Batch upload | Support multiple files at once | +| Validation feedback | Show errors inline before/during upload | +| Success confirmation | Show newly added tracks with option to queue | + +### Upload Configuration + +```bash +# Additional environment variables +MAX_UPLOAD_SIZE_MB=50 +MAX_BATCH_SIZE=20 +DUPLICATE_HANDLING=rename # skip | overwrite | rename +ALLOWED_EXTENSIONS=.mp3 # Could expand to .mp3,.flac,.wav +``` + +--- + +### Track +```typescript +interface Track { + id: string; // Unique identifier (hash of filepath or UUID) + filename: string; // Original filename + filepath: string; // Path relative to music directory + title: string; // From ID3 tag or filename + artist: string; // From ID3 tag + album: string; // From ID3 tag + duration: number; // Duration in seconds + hasArt: boolean; // Whether album art is available +} +``` + +### PlaybackState +```typescript +interface PlaybackState { + currentTrack: Track | null; + state: 'playing' | 'paused' | 'stopped'; + position: number; // Current position in seconds + volume: number; // 0-100 +} +``` + +### Queue +```typescript +interface Queue { + tracks: Track[]; // Ordered list of upcoming tracks + history: Track[]; // Recently played (optional) + loopMode: 'all' | 'one' | 'off'; + shuffled: boolean; +} +``` + +--- + +## Configuration + +### Environment Variables + +```bash +# Discord +DISCORD_BOT_TOKEN=your_bot_token_here +DISCORD_GUILD_ID=your_server_id +DISCORD_CHANNEL_ID=voice_channel_id + +# Paths +MUSIC_DIRECTORY=/music + +# Web Server +WEB_PORT=3000 +API_PORT=3001 + +# Optional +ADMIN_PASSWORD=optional_password +LOG_LEVEL=info +``` + +### Docker Compose Example + +```yaml +version: '3.8' + +services: + bot: + build: ./bot + restart: unless-stopped + environment: + - DISCORD_BOT_TOKEN=${DISCORD_BOT_TOKEN} + - DISCORD_GUILD_ID=${DISCORD_GUILD_ID} + - DISCORD_CHANNEL_ID=${DISCORD_CHANNEL_ID} + - API_URL=http://api:3001 + volumes: + - ./music:/music:ro + depends_on: + - api + + api: + build: ./api + restart: unless-stopped + ports: + - "3001:3001" + environment: + - MUSIC_DIRECTORY=/music + - BOT_INTERNAL_URL=http://bot:8080 + - MAX_UPLOAD_SIZE_MB=50 + - MAX_BATCH_SIZE=20 + volumes: + - ./music:/music # Read-write for uploads + - ./data:/data + + web: + build: ./web + restart: unless-stopped + ports: + - "3000:80" + depends_on: + - api +``` + +--- + +## Directory Structure + +``` +discord-dj-bot/ +├── docker-compose.yml +├── .env +├── .env.example +├── README.md +│ +├── bot/ +│ ├── Dockerfile +│ ├── package.json (or requirements.txt) +│ └── src/ +│ ├── index.js (or main.py) +│ ├── discord/ +│ │ ├── client.js +│ │ └── voice.js +│ └── api/ +│ └── internal.js +│ +├── api/ +│ ├── Dockerfile +│ ├── package.json (or requirements.txt) +│ └── src/ +│ ├── index.js +│ ├── routes/ +│ │ ├── tracks.js +│ │ ├── playback.js +│ │ ├── queue.js +│ │ └── upload.js +│ ├── services/ +│ │ ├── library.js +│ │ ├── bot-bridge.js +│ │ └── metadata.js +│ ├── middleware/ +│ │ └── upload.js # Multer/busboy config +│ └── websocket/ +│ └── handler.js +│ +├── web/ +│ ├── Dockerfile +│ ├── package.json +│ ├── public/ +│ └── src/ +│ ├── App.jsx +│ ├── components/ +│ │ ├── NowPlaying.jsx +│ │ ├── Queue.jsx +│ │ ├── Controls.jsx +│ │ ├── TrackList.jsx +│ │ └── UploadZone.jsx +│ └── hooks/ +│ ├── useWebSocket.js +│ └── useUpload.js +│ +├── music/ # Mount your MP3s here +│ ├── song1.mp3 +│ ├── song2.mp3 +│ └── ... +│ +└── data/ # Persistent data (optional) + └── library.db +``` + +--- + +## Bot-API Communication + +The bot and API need to communicate bidirectionally: + +### Option A: HTTP Internal API (Recommended for MVP) +- Bot exposes a simple HTTP server on internal network +- API calls bot endpoints to send commands +- Bot polls API or uses WebSocket for queue updates + +### Option B: Redis Pub/Sub +- Both services connect to a shared Redis instance +- Commands and state updates flow through Redis channels + +### Option C: Shared SQLite + File Watching +- State stored in SQLite database +- Bot watches for changes and reacts +- Simple but less real-time + +--- + +## Security Considerations + +1. **Bot Token**: Never commit to version control; use environment variables +2. **Web Interface**: Consider adding basic auth for public-facing deployments +3. **Internal Network**: Bot and API should communicate over Docker internal network only +4. **File Access**: Mount music directory as read-only for bot; API needs write access for uploads + +### Upload Security + +5. **File Type Validation**: Validate MIME type AND file extension; optionally verify MP3 magic bytes +6. **File Size Limits**: Enforce maximum file size to prevent disk exhaustion attacks (100mb) +7. **Filename Sanitization**: Strip path traversal characters (`../`, `./`), null bytes, and special characters +8. **Rate Limiting**: Limit uploads per IP/session to prevent abuse +9. **Disk Space Monitoring**: Check available disk space before accepting uploads + +### Recommended Upload Middleware + +**Node.js:** +```javascript +// Using multer with limits +const upload = multer({ + dest: '/tmp/uploads', + limits: { + fileSize: 50 * 1024 * 1024, // 50MB + files: 20 + }, + fileFilter: (req, file, cb) => { + if (file.mimetype !== 'audio/mpeg') { + return cb(new Error('Only MP3 files allowed')); + } + cb(null, true); + } +}); +``` + +**Python:** +```python +# Using FastAPI with validation +from fastapi import UploadFile + +async def validate_upload(file: UploadFile): + if file.content_type != "audio/mpeg": + raise HTTPException(400, "Only MP3 files allowed") + if file.size > 50 * 1024 * 1024: + raise HTTPException(400, "File too large") +``` + +--- + +## Discord Setup Guide + +Before deploying the bot, you'll need to create a Discord Application and Bot account. This is free and takes about 5-10 minutes. + +### Step 1: Create a Discord Application + +1. Go to the [Discord Developer Portal](https://discord.com/developers/applications) +2. Log in with your Discord account +3. Click **"New Application"** (top right) +4. Enter a name for your application (e.g., "DJ Bot") +5. Accept the terms of service and click **"Create"** + +You'll land on the "General Information" page. You can optionally add a description and app icon here. + +### Step 2: Create the Bot User + +1. In the left sidebar, click **"Bot"** +2. Click **"Add Bot"** and confirm +3. (Optional) Customize the bot's username and avatar—this is what users see in Discord + +**Important Bot Settings:** +| Setting | Value | Why | +|---------|-------|-----| +| Public Bot | Off (recommended) | Prevents others from adding your bot to their servers | +| Requires OAuth2 Code Grant | Off | Not needed for this use case | +| Presence Intent | Off | Not needed | +| Server Members Intent | Off | Not needed | +| Message Content Intent | Off | Not needed (unless adding text commands later) | + +### Step 3: Get Your Bot Token + +1. On the **"Bot"** page, find the **"Token"** section +2. Click **"Reset Token"** (or "Copy" if visible) +3. **Copy and save this token securely**—you won't be able to see it again + +``` +⚠️ CRITICAL: Never share your bot token or commit it to version control. + Anyone with this token has full control of your bot. + If leaked, immediately reset it in the Developer Portal. +``` + +This token goes in your `.env` file as `DISCORD_BOT_TOKEN`. + +### Step 4: Configure OAuth2 Permissions + +1. In the left sidebar, click **"OAuth2"** → **"URL Generator"** +2. Under **"Scopes"**, select: + - `bot` +3. Under **"Bot Permissions"**, select: + - `Connect` — Join voice channels + - `Speak` — Play audio in voice channels + - `View Channels` — See available channels + +**Minimal permissions integer:** `3145728` + +The generated URL at the bottom will look like: +``` +https://discord.com/api/oauth2/authorize?client_id=YOUR_CLIENT_ID&permissions=3145728&scope=bot +``` + +### Step 5: Invite the Bot to Your Server + +1. Copy the generated OAuth2 URL from Step 4 +2. Paste it in your browser +3. Select the server you want to add the bot to (you need "Manage Server" permission) +4. Click **"Authorize"** +5. Complete the CAPTCHA + +The bot should now appear in your server's member list (it will be offline until you run it). + +### Step 6: Get Server and Channel IDs + +You'll need the Guild (server) ID and Voice Channel ID for your configuration. + +**Enable Developer Mode first:** +1. Open Discord Settings (gear icon) +2. Go to **"App Settings"** → **"Advanced"** +3. Enable **"Developer Mode"** + +**Get the Guild ID:** +1. Right-click on your server icon in the server list +2. Click **"Copy Server ID"** + +This goes in your `.env` file as `DISCORD_GUILD_ID`. + +**Get the Voice Channel ID:** +1. Right-click on the voice channel where the bot should play music +2. Click **"Copy Channel ID"** + +This goes in your `.env` file as `DISCORD_CHANNEL_ID`. + +### Summary: What You Need + +After completing setup, you should have these three values: + +| Value | Example | Environment Variable | +|-------|---------|---------------------| +| Bot Token | `MTIzNDU2Nzg5MDEyMzQ1Njc4OQ.Gh7K2j.xxxxx...` | `DISCORD_BOT_TOKEN` | +| Guild ID | `123456789012345678` | `DISCORD_GUILD_ID` | +| Channel ID | `987654321098765432` | `DISCORD_CHANNEL_ID` | + +### Troubleshooting + +| Issue | Solution | +|-------|----------| +| Bot won't connect | Verify token is correct and hasn't been reset | +| Bot can't join voice | Check bot has `Connect` permission in that channel | +| Bot joins but no audio | Check bot has `Speak` permission; verify audio encoding setup | +| "Bot is not in this guild" | Confirm Guild ID is correct; re-invite bot if needed | +| Can't see channel | Bot needs `View Channels` permission | + +### Discord API Rate Limits + +Be aware of Discord's rate limits when developing: + +| Action | Limit | +|--------|-------| +| Global requests | 50 requests/second | +| Gateway connects | 1 per 5 seconds | +| Voice state updates | ~5 per minute recommended | + +The bot libraries (`discord.py`, `discord.js`) handle most rate limiting automatically. + +--- + +## Deployment Checklist + +### Discord Setup (see detailed guide above) +- [ ] Create Discord application at https://discord.com/developers +- [ ] Create bot user and configure settings +- [ ] Copy and securely store bot token +- [ ] Generate OAuth2 invite URL with correct permissions +- [ ] Invite bot to your server +- [ ] Copy Guild ID and Voice Channel ID + +### Server Setup +- [ ] Provision VPS (recommended: 1GB+ RAM, Ubuntu 22.04+) +- [ ] Install Docker and Docker Compose +- [ ] Configure firewall (allow ports 3000, 3001 or your chosen ports) +- [ ] (Optional) Set up domain name and DNS + +### Application Deployment +- [ ] Clone repository to VPS +- [ ] Copy `.env.example` to `.env` +- [ ] Fill in Discord credentials (token, guild ID, channel ID) +- [ ] Configure upload limits and other settings +- [ ] Upload initial MP3 files to `./music` directory +- [ ] Run `docker-compose build` +- [ ] Run `docker-compose up -d` +- [ ] Verify bot comes online in Discord +- [ ] Test web interface loads correctly +- [ ] Test playback controls work +- [ ] Test file upload works + +### Production Hardening (Optional) +- [ ] Set up reverse proxy (nginx/Caddy) with SSL +- [ ] Configure `ADMIN_PASSWORD` for web interface +- [ ] Set up log rotation +- [ ] Configure automatic container restarts +- [ ] Set up monitoring/alerting (optional) + +--- + +## Future Considerations + +- **Horizontal scaling**: If needed, the web frontend can scale independently +- **Mobile app**: API is designed to support any client +- **Multiple bots**: Could extend to support multiple Discord servers +- **Transcoding**: Convert non-MP3 files on the fly +- **External storage**: Support S3 or other cloud storage for music files