From d8952577ad523e28cbc1c0a97aca25150900f78d Mon Sep 17 00:00:00 2001
From: Top1055 <123alexfeetham@gmail.com>
Date: Sat, 29 Nov 2025 17:02:15 +0000
Subject: [PATCH 1/2] Quick wins: Lower default volume to 25% and add file
upload support
- Scale volume by 0.25x to prevent earrape (user still sees 0-200%)
- Add support for direct audio file URL links (.mp3, .mp4, etc.)
- New =playfile command for Discord file uploads
- Supports MP3, MP4, WAV, OGG, FLAC, M4A, WEBM, AAC, OPUS formats
---
PRODUCTION_ROADMAP.md | 849 ++++++++++++++++++++++++++++++++++++++++
README.md | 2 -
cogs/music/main.py | 69 +++-
cogs/music/queue.py | 5 +-
cogs/music/translate.py | 11 +-
web_dhasboard_plan.md | 353 -----------------
6 files changed, 930 insertions(+), 359 deletions(-)
create mode 100644 PRODUCTION_ROADMAP.md
delete mode 100644 web_dhasboard_plan.md
diff --git a/PRODUCTION_ROADMAP.md b/PRODUCTION_ROADMAP.md
new file mode 100644
index 0000000..8cefbd5
--- /dev/null
+++ b/PRODUCTION_ROADMAP.md
@@ -0,0 +1,849 @@
+# Groovy-Zilean Production Roadmap
+
+**Goal:** Transform groovy-zilean from a personal project into a production-ready Discord music bot with a web dashboard.
+
+**Philosophy:** Production-quality architecture with manageable complexity for a solo developer. No overkill, no shortcuts.
+
+---
+
+## Table of Contents
+1. [Architecture Overview](#architecture-overview)
+2. [Tech Stack Decisions](#tech-stack-decisions)
+3. [Development Phases](#development-phases)
+4. [Python Environment Setup](#python-environment-setup)
+5. [Why This Approach](#why-this-approach)
+6. [Quick Reference](#quick-reference)
+
+---
+
+## Architecture Overview
+
+### The Winning Design: Database-Mediated Architecture
+
+```
+┌─────────────┐ HTTP/SSE ┌──────────────┐
+│ Browser │◄────────────►│ FastAPI │
+│ (HTMX HTML) │ │ Backend │
+└─────────────┘ └──────┬───────┘
+ │
+ ┌──────▼───────┐ ┌─────────────┐
+ │ PostgreSQL │◄────────│ Discord Bot │
+ │ Database │ │ (discord.py)│
+ └──────────────┘ └─────────────┘
+```
+
+### Key Principles
+
+1. **Decoupled Services**
+ - Bot and web can restart independently
+ - No tight coupling via IPC
+ - Database is the single source of truth
+
+2. **Simple Frontend (Initially)**
+ - HTMX for interactivity (no build step)
+ - Jinja2 templates (server-side rendering)
+ - Tailwind CSS via CDN (beautiful, no npm)
+ - **Optional:** Upgrade to React later when ready
+
+3. **Production-Ready Backend**
+ - PostgreSQL for reliability
+ - FastAPI for modern Python web
+ - Discord OAuth2 for authentication
+ - Connection pooling, proper error handling
+
+---
+
+## Tech Stack Decisions
+
+### Backend
+
+| Component | Choice | Why |
+|-----------|--------|-----|
+| **Bot Framework** | discord.py 2.6.4+ | Industry standard, hybrid commands |
+| **Web Framework** | FastAPI | Modern, async, auto-docs, large community |
+| **Database** | PostgreSQL 14+ | Production-ready, ACID compliance, better than SQLite |
+| **Music Extraction** | yt-dlp | Actively maintained, multi-platform support |
+| **Auth** | Discord OAuth2 | Native Discord integration |
+| **ORM** | SQLAlchemy (optional) | Or use asyncpg directly for simplicity |
+
+### Frontend (Phase 1)
+
+| Component | Choice | Why |
+|-----------|--------|-----|
+| **Templates** | Jinja2 | Server-side rendering, no build step |
+| **Interactivity** | HTMX | Modern interactivity without React complexity |
+| **Styling** | Tailwind CSS (CDN) | Beautiful UI, no build process |
+| **Real-time** | Server-Sent Events | Simple polling/updates, no WebSocket complexity |
+
+### Frontend (Phase 2 - Optional)
+
+| Component | Choice | Why |
+|-----------|--------|-----|
+| **Framework** | React 18 | If you need more complex UI later |
+| **Real-time** | WebSocket | For true real-time when needed |
+| **State** | Zustand/Context | Simpler than Redux |
+
+### Infrastructure
+
+| Component | Choice | Why |
+|-----------|--------|-----|
+| **Python Version** | 3.11 or 3.12 | Modern features, better performance |
+| **Environment** | venv | Isolated dependencies, no PATH conflicts |
+| **Process Manager** | systemd | Reliable, built into Linux |
+| **Reverse Proxy** | nginx | Standard, handles SSL, static files |
+
+---
+
+## Development Phases
+
+### Phase 0: Current State ✅
+- Working Discord bot with music playback
+- SQLite database
+- Hybrid commands (slash + prefix)
+- 17 audio effects
+- Queue, loop, shuffle functionality
+- Spotify integration
+
+### Phase 1: Quick Wins (1-2 hours)
+
+**Goal:** Immediate improvements for better UX
+
+Tasks:
+1. **Lower default volume from 100% to 25%**
+ - Change default in `queue.py:119`
+ - Scale volume command display (user sees 0-200%, internally 0-50%)
+ - Prevents earrape for new users
+
+2. **Add .mp3 and .mp4 file support**
+ - Extend `translate.py` to detect direct file URLs
+ - Support HTTP/HTTPS links to audio files
+ - Validate file type before processing
+
+**Files to modify:**
+- `cogs/music/queue.py`
+- `cogs/music/translate.py`
+
+---
+
+### Phase 2: Code Refactoring (4-6 hours)
+
+**Goal:** Clean, maintainable, documented codebase
+
+#### 2.1 Database Abstraction
+Create `cogs/music/db_manager.py`:
+- Database connection class with context manager
+- Connection pooling preparation
+- Centralize all SQL queries
+- Remove scattered `sqlite3.connect()` calls
+
+#### 2.2 Configuration Management
+Update `config.py`:
+- Use environment variables for secrets
+- Create `.env.example` template
+- Remove hardcoded credentials
+- Add validation for required config
+
+#### 2.3 Code Organization
+```
+groovy-zilean/
+├── bot/
+│ ├── __init__.py
+│ ├── bot.py # Main bot class
+│ └── cogs/
+│ └── music/
+│ ├── __init__.py
+│ ├── commands.py # User-facing commands
+│ ├── player.py # Playback logic
+│ ├── queue.py # Queue management
+│ ├── effects.py # Audio effects
+│ ├── db_manager.py # Database abstraction
+│ └── translate.py # URL/playlist parsing
+├── web/
+│ ├── __init__.py # (Future web app)
+├── shared/
+│ ├── __init__.py
+│ ├── config.py # Shared configuration
+│ └── models.py # Data models
+├── main.py # Entry point
+├── requirements.txt
+├── .env.example
+└── README.md
+```
+
+#### 2.4 Error Handling & Logging
+- Wrap all commands in try/except
+- User-friendly error messages
+- Proper logging setup (rotating file logs)
+- Debug mode toggle
+
+#### 2.5 Type Hints & Documentation
+- Add type hints to all functions
+- Docstrings for all classes/methods
+- Inline comments for complex logic
+
+**Expected outcome:**
+- Easy to navigate codebase
+- No secrets in code
+- Consistent patterns throughout
+
+---
+
+### Phase 3: PostgreSQL Migration (3-4 hours)
+
+**Goal:** Production-ready database layer
+
+#### 3.1 Local PostgreSQL Setup
+```bash
+# Install PostgreSQL
+sudo apt update
+sudo apt install postgresql postgresql-contrib
+
+# Create database and user
+sudo -u postgres psql
+CREATE DATABASE groovy_zilean;
+CREATE USER groovy WITH PASSWORD 'your_password';
+GRANT ALL PRIVILEGES ON DATABASE groovy_zilean TO groovy;
+```
+
+#### 3.2 Database Schema Design
+```sql
+-- servers table
+CREATE TABLE servers (
+ server_id BIGINT PRIMARY KEY,
+ is_playing BOOLEAN DEFAULT FALSE,
+ song_name TEXT,
+ song_url TEXT,
+ song_thumbnail TEXT,
+ loop_mode VARCHAR(10) DEFAULT 'off',
+ volume INTEGER DEFAULT 25, -- NEW default!
+ effect VARCHAR(20) DEFAULT 'none',
+ song_start_time DOUBLE PRECISION DEFAULT 0,
+ song_duration INTEGER DEFAULT 0,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+-- songs/queue table
+CREATE TABLE songs (
+ id SERIAL PRIMARY KEY,
+ server_id BIGINT NOT NULL REFERENCES servers(server_id) ON DELETE CASCADE,
+ song_link TEXT,
+ queued_by TEXT,
+ position INTEGER NOT NULL,
+ title TEXT,
+ thumbnail TEXT,
+ duration INTEGER,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ UNIQUE(server_id, position)
+);
+
+-- Future: users table for web auth
+CREATE TABLE users (
+ discord_id BIGINT PRIMARY KEY,
+ username TEXT,
+ avatar TEXT,
+ access_token TEXT,
+ refresh_token TEXT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ last_login TIMESTAMP
+);
+
+-- Future: permissions table
+CREATE TABLE permissions (
+ id SERIAL PRIMARY KEY,
+ server_id BIGINT REFERENCES servers(server_id),
+ user_id BIGINT,
+ role_id BIGINT,
+ can_play BOOLEAN DEFAULT TRUE,
+ can_skip BOOLEAN DEFAULT FALSE,
+ can_clear BOOLEAN DEFAULT FALSE,
+ can_modify_settings BOOLEAN DEFAULT FALSE
+);
+```
+
+#### 3.3 Migration Script
+Create `scripts/migrate_to_postgres.py`:
+- Read all data from SQLite
+- Insert into PostgreSQL
+- Validate migration
+- Backup SQLite file
+
+#### 3.4 Update Database Code
+Replace all `sqlite3` calls with `asyncpg` or `psycopg3`:
+```python
+# Old (SQLite)
+conn = sqlite3.connect(db_path)
+cursor = conn.cursor()
+
+# New (PostgreSQL with asyncpg)
+async with pool.acquire() as conn:
+ result = await conn.fetch("SELECT * FROM servers WHERE server_id = $1", server_id)
+```
+
+#### 3.5 Connection Pooling
+```python
+# In bot startup
+self.db_pool = await asyncpg.create_pool(
+ host='localhost',
+ database='groovy_zilean',
+ user='groovy',
+ password=os.getenv('DB_PASSWORD'),
+ min_size=5,
+ max_size=20
+)
+```
+
+**Expected outcome:**
+- Reliable database with ACID guarantees
+- Better concurrent access handling
+- Ready for multi-server production load
+
+---
+
+### Phase 4: Web Dashboard (20-30 hours)
+
+**Goal:** User-friendly web interface for bot control
+
+#### 4.1 FastAPI Backend Setup
+
+**Project structure:**
+```
+web/
+├── __init__.py
+├── main.py # FastAPI app
+├── routes/
+│ ├── __init__.py
+│ ├── auth.py # Discord OAuth2
+│ ├── servers.py # Server list/select
+│ └── playback.py # Queue/controls
+├── templates/
+│ ├── base.html
+│ ├── index.html
+│ ├── dashboard.html
+│ └── components/
+│ ├── queue.html
+│ └── controls.html
+├── static/
+│ ├── css/
+│ └── js/
+└── dependencies.py # Auth dependencies
+```
+
+**Core dependencies:**
+```bash
+pip install fastapi uvicorn jinja2 python-multipart httpx
+```
+
+#### 4.2 Discord OAuth2 Authentication
+
+**Flow:**
+1. User clicks "Login with Discord"
+2. Redirect to Discord OAuth
+3. Discord redirects back with code
+4. Exchange code for token
+5. Fetch user info + guilds
+6. Create session
+7. Show dashboard with user's servers
+
+**Implementation:**
+```python
+# routes/auth.py
+@router.get("/login")
+async def login():
+ # Redirect to Discord OAuth
+ discord_auth_url = (
+ f"https://discord.com/api/oauth2/authorize"
+ f"?client_id={DISCORD_CLIENT_ID}"
+ f"&redirect_uri={REDIRECT_URI}"
+ f"&response_type=code"
+ f"&scope=identify guilds"
+ )
+ return RedirectResponse(discord_auth_url)
+
+@router.get("/callback")
+async def callback(code: str):
+ # Exchange code for token
+ # Fetch user info
+ # Create session
+ # Redirect to dashboard
+```
+
+#### 4.3 HTMX Frontend
+
+**Example dashboard with HTMX:**
+```html
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+**Why HTMX is perfect here:**
+- No JavaScript needed for interactivity
+- Server renders everything (simpler)
+- Auto-updates with `hx-trigger="every Xs"`
+- Progressive enhancement (works without JS)
+
+#### 4.4 API Endpoints
+
+**Read endpoints:**
+```python
+GET /api/servers # User's servers (with bot)
+GET /api/servers/{id}/queue # Current queue
+GET /api/servers/{id}/status # Now playing, volume, etc.
+```
+
+**Write endpoints:**
+```python
+POST /api/servers/{id}/play # Add song (body: {query: "..."})
+POST /api/servers/{id}/skip # Skip current song
+POST /api/servers/{id}/volume # Set volume (body: {volume: 150})
+POST /api/servers/{id}/effect # Set effect (body: {effect: "nightcore"})
+POST /api/servers/{id}/loop # Set loop mode (body: {mode: "queue"})
+POST /api/servers/{id}/shuffle # Shuffle queue
+POST /api/servers/{id}/clear # Clear queue
+```
+
+**Implementation example:**
+```python
+# routes/playback.py
+@router.post("/api/servers/{server_id}/skip")
+async def skip_song(
+ server_id: int,
+ user: User = Depends(get_current_user),
+ db = Depends(get_db)
+):
+ # 1. Check user is in server
+ if server_id not in user.guild_ids:
+ raise HTTPException(403, "Not in this server")
+
+ # 2. Check permissions (future)
+ # if not has_permission(user, server_id, "can_skip"):
+ # raise HTTPException(403, "No permission")
+
+ # 3. Write command to database
+ await db.execute(
+ "INSERT INTO commands (server_id, action, user_id) VALUES ($1, $2, $3)",
+ server_id, "skip", user.id
+ )
+
+ # 4. Return updated queue
+ return await get_queue(server_id, db)
+```
+
+#### 4.5 Bot Integration (Command Processing)
+
+**Add to bot:**
+```python
+# In bot.py or new cogs/web_commands.py
+@tasks.loop(seconds=1)
+async def process_web_commands(self):
+ """Process commands from web dashboard"""
+ async with self.db_pool.acquire() as conn:
+ # Fetch unprocessed commands
+ commands = await conn.fetch(
+ "SELECT * FROM commands WHERE processed = FALSE"
+ )
+
+ for cmd in commands:
+ server_id = cmd['server_id']
+ action = cmd['action']
+ data = cmd['data']
+
+ guild = self.get_guild(server_id)
+ if not guild or not guild.voice_client:
+ continue
+
+ # Execute command
+ if action == "skip":
+ guild.voice_client.stop()
+ elif action == "volume":
+ # Set volume in database, next song picks it up
+ await queue.set_volume(server_id, data['volume'])
+ elif action == "play":
+ # Queue song from web
+ # ... (use existing play logic)
+
+ # Mark as processed
+ await conn.execute(
+ "UPDATE commands SET processed = TRUE WHERE id = $1",
+ cmd['id']
+ )
+```
+
+#### 4.6 Permissions System
+
+**Basic implementation:**
+```python
+# Check if user can control bot
+async def can_control_bot(user_id: int, server_id: int, action: str) -> bool:
+ # Check if user is in voice channel with bot
+ # Check if user has DJ role (configurable per server)
+ # Check specific permission for action
+ # Default: anyone in VC can control
+ pass
+```
+
+**Advanced (Phase 5):**
+- Role-based permissions
+- User-specific permissions
+- Configurable via web dashboard
+
+**Expected outcome:**
+- Beautiful, functional web dashboard
+- Discord OAuth login
+- Real-time queue display
+- Full playback control from browser
+- Works on mobile
+
+---
+
+### Phase 5: Permissions & Production (4-6 hours)
+
+**Goal:** Production-ready deployment
+
+#### 5.1 Permission System
+- DJ role configuration
+- Per-server permission settings
+- Web UI for permission management
+
+#### 5.2 Rate Limiting
+```python
+from slowapi import Limiter
+limiter = Limiter(key_func=get_remote_address)
+
+@app.post("/api/play")
+@limiter.limit("10/minute") # Max 10 songs per minute
+async def play_song(...):
+ ...
+```
+
+#### 5.3 Logging & Monitoring
+```python
+import logging
+from logging.handlers import RotatingFileHandler
+
+# Setup logging
+handler = RotatingFileHandler(
+ 'logs/bot.log',
+ maxBytes=10_000_000, # 10MB
+ backupCount=5
+)
+logging.basicConfig(
+ handlers=[handler],
+ level=logging.INFO,
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+)
+```
+
+#### 5.4 Systemd Services
+```ini
+# /etc/systemd/system/groovy-bot.service
+[Unit]
+Description=Groovy Zilean Discord Bot
+After=network.target postgresql.service
+
+[Service]
+Type=simple
+User=groovy
+WorkingDirectory=/home/groovy/groovy-zilean
+Environment="PATH=/home/groovy/groovy-zilean/venv/bin"
+ExecStart=/home/groovy/groovy-zilean/venv/bin/python main.py
+Restart=always
+RestartSec=10
+
+[Install]
+WantedBy=multi-user.target
+```
+
+```ini
+# /etc/systemd/system/groovy-web.service
+[Unit]
+Description=Groovy Zilean Web Dashboard
+After=network.target postgresql.service
+
+[Service]
+Type=simple
+User=groovy
+WorkingDirectory=/home/groovy/groovy-zilean
+Environment="PATH=/home/groovy/groovy-zilean/venv/bin"
+ExecStart=/home/groovy/groovy-zilean/venv/bin/uvicorn web.main:app --host 0.0.0.0 --port 8000
+Restart=always
+RestartSec=10
+
+[Install]
+WantedBy=multi-user.target
+```
+
+#### 5.5 Nginx Configuration
+```nginx
+server {
+ listen 80;
+ server_name groovy.yourdomain.com;
+
+ # Redirect to HTTPS
+ return 301 https://$host$request_uri;
+}
+
+server {
+ listen 443 ssl http2;
+ server_name groovy.yourdomain.com;
+
+ ssl_certificate /etc/letsencrypt/live/groovy.yourdomain.com/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/groovy.yourdomain.com/privkey.pem;
+
+ location / {
+ proxy_pass http://localhost:8000;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ location /static {
+ alias /home/groovy/groovy-zilean/web/static;
+ expires 30d;
+ }
+}
+```
+
+#### 5.6 Database Backups
+```bash
+#!/bin/bash
+# scripts/backup_db.sh
+
+BACKUP_DIR="/home/groovy/backups"
+DATE=$(date +%Y%m%d_%H%M%S)
+
+pg_dump groovy_zilean > "$BACKUP_DIR/groovy_zilean_$DATE.sql"
+
+# Keep only last 7 days
+find $BACKUP_DIR -name "groovy_zilean_*.sql" -mtime +7 -delete
+```
+
+Add to crontab:
+```bash
+0 2 * * * /home/groovy/groovy-zilean/scripts/backup_db.sh
+```
+
+#### 5.7 Environment Variables
+```bash
+# .env (NEVER commit this!)
+DISCORD_TOKEN=your_bot_token_here
+DISCORD_CLIENT_ID=your_client_id
+DISCORD_CLIENT_SECRET=your_client_secret
+SPOTIFY_CLIENT_ID=your_spotify_id
+SPOTIFY_CLIENT_SECRET=your_spotify_secret
+DB_PASSWORD=your_db_password
+SECRET_KEY=random_secret_for_sessions
+ENVIRONMENT=production
+```
+
+**Expected outcome:**
+- Production-ready deployment
+- Automatic restarts on failure
+- HTTPS enabled
+- Automated backups
+- Proper logging
+
+---
+
+## Python Environment Setup
+
+### Avoiding Python Version Hell
+
+**Step 1: Install Python 3.12**
+```bash
+# On Debian/Ubuntu
+sudo apt update
+sudo apt install python3.12 python3.12-venv python3.12-dev
+
+# Verify installation
+python3.12 --version
+```
+
+**Step 2: Create Virtual Environment**
+```bash
+cd ~/coding/groovy-zilean
+
+# Create venv (only once)
+python3.12 -m venv venv
+
+# Activate (every time you work on project)
+source venv/bin/activate
+
+# Your prompt should change to show (venv)
+```
+
+**Step 3: Install Dependencies**
+```bash
+# Make sure venv is activated!
+pip install --upgrade pip
+pip install -r requirements.txt
+```
+
+**Step 4: Add to .gitignore**
+```
+venv/
+__pycache__/
+*.pyc
+.env
+*.db
+logs/
+```
+
+**Helpful Aliases (add to ~/.bashrc)**
+```bash
+alias groovy='cd ~/coding/groovy-zilean && source venv/bin/activate'
+```
+
+Then just type `groovy` to activate your environment!
+
+---
+
+## Why This Approach?
+
+### Rejected: discord-ext-ipc + Quart
+❌ **discord-ext-ipc is unmaintained** (last update 2+ years ago)
+❌ Tight coupling between bot and web
+❌ Both processes must run together
+❌ Quart less popular than FastAPI
+❌ Still need polling for real-time updates
+
+### Rejected: FastAPI + React + Redis + WebSocket
+❌ **Overkill for solo developer**
+❌ Too many moving parts (4+ services)
+❌ npm/node_modules complexity
+❌ Requires learning React well
+❌ WebSocket complexity for minimal gain
+
+### Chosen: FastAPI + PostgreSQL + HTMX ✅
+✅ **Production-quality architecture**
+✅ Decoupled services (independent restarts)
+✅ Modern, well-maintained tools
+✅ No frontend build step (initially)
+✅ Easy upgrade path to React later
+✅ Manageable complexity for solo dev
+✅ Database as source of truth
+✅ All Python backend
+
+---
+
+## Quick Reference
+
+### Daily Development Workflow
+```bash
+# Activate environment
+cd ~/coding/groovy-zilean
+source venv/bin/activate
+
+# Run bot
+python main.py
+
+# Run web (in another terminal)
+source venv/bin/activate
+uvicorn web.main:app --reload --port 8000
+
+# Run tests (future)
+pytest
+
+# Database migrations (future)
+alembic upgrade head
+```
+
+### Project Commands
+```bash
+# Install new package
+pip install package_name
+pip freeze > requirements.txt
+
+# Database
+psql groovy_zilean # Connect to database
+pg_dump groovy_zilean > backup.sql # Backup
+
+# Systemd
+sudo systemctl start groovy-bot
+sudo systemctl status groovy-bot
+sudo journalctl -u groovy-bot -f # View logs
+```
+
+### File Locations
+- **Bot entry point:** `main.py`
+- **Config:** `shared/config.py` + `.env`
+- **Database:** PostgreSQL (not file-based)
+- **Logs:** `logs/bot.log`, `logs/web.log`
+- **Web templates:** `web/templates/`
+
+---
+
+## Success Metrics
+
+By the end of this roadmap, you'll have:
+
+✅ Clean, maintainable codebase
+✅ Production-ready database
+✅ Web dashboard with Discord auth
+✅ Mobile-friendly interface
+✅ Automated deployment
+✅ Backup system
+✅ Proper error handling & logging
+✅ Permission system
+✅ Rate limiting
+✅ No earrape (25% default volume!)
+✅ .mp3/.mp4 file support
+
+**Most importantly:** A bot that real servers can use, that you can maintain solo, and that you're proud of!
+
+---
+
+## Next Steps
+
+1. **Today:** Quick wins (volume + file support)
+2. **This week:** Refactoring
+3. **Next week:** PostgreSQL migration
+4. **Week after:** Web dashboard MVP
+5. **Final week:** Production deployment
+
+Let's build something awesome! 🎵⏱️
diff --git a/README.md b/README.md
index b3f6da1..e69de29 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +0,0 @@
-# serenity-bot
-A music bot, will replace my groovy zilean bot and hopefully be written slightly more clearer
diff --git a/cogs/music/main.py b/cogs/music/main.py
index fe3504a..c3c4c5d 100644
--- a/cogs/music/main.py
+++ b/cogs/music/main.py
@@ -141,6 +141,72 @@ class music(commands.Cog):
await queue.play(ctx)
+ @commands.command(
+ help="Upload and play an audio file (MP3, MP4, WAV, etc.)",
+ aliases=['pf', 'file'])
+ async def playfile(self, ctx: Context):
+ """Play an uploaded audio file from Discord attachment"""
+ if ctx.guild is None:
+ await ctx.send("❌ This command must be used in a server!")
+ return
+
+ # Check if there's an attachment
+ if not ctx.message.attachments:
+ await ctx.send(
+ "❌ No file attached! Please upload an audio file with your message.\n"
+ "**Supported formats:** MP3, MP4, WAV, OGG, FLAC, M4A, WEBM, AAC, OPUS"
+ )
+ return
+
+ server = ctx.guild.id
+ attachment = ctx.message.attachments[0]
+
+ # Validate file extension
+ audio_extensions = ('.mp3', '.mp4', '.wav', '.ogg', '.flac', '.m4a', '.webm', '.aac', '.opus')
+ if not any(attachment.filename.lower().endswith(ext) for ext in audio_extensions):
+ await ctx.send(
+ f"❌ Invalid file type: `{attachment.filename}`\n"
+ f"**Supported formats:** MP3, MP4, WAV, OGG, FLAC, M4A, WEBM, AAC, OPUS"
+ )
+ return
+
+ await util.join_vc(ctx)
+ await ctx.message.add_reaction('📎')
+
+ msg = await ctx.send(f"Processing file: `{attachment.filename}`...")
+
+ # Discord provides a CDN URL for the attachment
+ file_url = attachment.url
+
+ # Use translate to process the file URL (yt-dlp handles direct URLs)
+ audio = await translate.main(file_url, self.sp)
+
+ await msg.delete()
+
+ if len(audio) == 0:
+ await ctx.message.add_reaction('🚫')
+ await ctx.send("❌ Failed to process the audio file!")
+ return
+
+ # Override title with filename if yt-dlp didn't get a good title
+ if audio[0]['title'] == 'Unknown' or not audio[0]['title']:
+ audio[0]['title'] = attachment.filename
+
+ # Queue the file
+ audio[0]['position'] = await queue.add_song(
+ server,
+ audio[0],
+ ctx.author.display_name)
+
+ await util.queue_message(ctx, audio[0])
+
+ if await queue.is_server_playing(server):
+ return
+
+ await queue.update_server(server, True)
+ await queue.play(ctx)
+
+
@commands.command(
help="Queue a song to play next (top of queue)",
aliases=['pt', 'pn', 'playnext'])
@@ -347,8 +413,9 @@ class music(commands.Cog):
new_vol = await queue.set_volume(server.id, level)
# Update the current playing song's volume if something is playing
+ # Scale down by 0.25 to match queue.py playback scaling
if ctx.voice_client and ctx.voice_client.source:
- ctx.voice_client.source.volume = new_vol / 100.0
+ ctx.voice_client.source.volume = new_vol / 100.0 * 0.25
# Pick an emoji based on volume
if new_vol == 0:
diff --git a/cogs/music/queue.py b/cogs/music/queue.py
index 62f9266..96eac30 100644
--- a/cogs/music/queue.py
+++ b/cogs/music/queue.py
@@ -497,7 +497,10 @@ async def play(ctx):
return
try:
- vol = await get_volume(server_id) / 100.0
+ # Scale volume down to prevent earrape
+ # User sees 0-200%, but internally we scale by 0.25
+ # So user's 100% = 0.25 actual volume (25%)
+ vol = await get_volume(server_id) / 100.0 * 0.25
fx = await get_effect(server_id)
opts = get_effect_options(fx)
diff --git a/cogs/music/translate.py b/cogs/music/translate.py
index 074d342..7da3503 100644
--- a/cogs/music/translate.py
+++ b/cogs/music/translate.py
@@ -27,7 +27,7 @@ async def main(url, sp):
#url = url.lower()
# Check if link or search
- if url.startswith("https://") is False:
+ if not url.startswith("https://") and not url.startswith("http://"):
return await search_song(url)
#TODO add better regex or something
@@ -44,7 +44,14 @@ async def main(url, sp):
youtube_song = 'watch?v=' in url or 'youtu.be/' in url
youtube_playlist = 'playlist?list=' in url
- if soundcloud_song or youtube_song:
+ # Check for direct audio/video file URLs
+ # Supported formats: mp3, mp4, wav, ogg, flac, m4a, webm, aac, opus
+ audio_extensions = ('.mp3', '.mp4', '.wav', '.ogg', '.flac', '.m4a', '.webm', '.aac', '.opus')
+ is_direct_file = any(url.lower().endswith(ext) for ext in audio_extensions)
+ # Also check for URLs with query parameters (e.g., file.mp3?download=true)
+ is_direct_file = is_direct_file or any(ext in url.lower() for ext in audio_extensions)
+
+ if soundcloud_song or youtube_song or is_direct_file:
return await song_download(url)
if youtube_playlist:
diff --git a/web_dhasboard_plan.md b/web_dhasboard_plan.md
deleted file mode 100644
index d15293f..0000000
--- a/web_dhasboard_plan.md
+++ /dev/null
@@ -1,353 +0,0 @@
-# Astro Bot Web Dashboard Architecture
-
-## Overview
-A real-time web dashboard for controlling the Discord music bot from a browser. Users can manage queues, adjust settings, and control playback without Discord.
-
-## Tech Stack
-
-### Backend (Flask/FastAPI)
-**Recommended: FastAPI** (modern, async, WebSocket support built-in)
-
-```python
-# Dependencies
-fastapi==0.104.1
-uvicorn[standard]==0.24.0
-websockets==12.0
-python-socketio==5.10.0 # For real-time updates
-aioredis==2.0.1 # For session management
-```
-
-**Why FastAPI:**
-- Native async support (works well with discord.py)
-- Built-in WebSocket support
-- Auto-generated API docs
-- Fast performance
-
-### Frontend
-**Recommended: React + Tailwind CSS**
-
-```
-React 18
-Tailwind CSS
-Socket.IO client (for real-time)
-Axios (for API calls)
-```
-
-**Alternative (simpler):** Vanilla JS + Tailwind if you want less complexity
-
----
-
-## Architecture Diagram
-
-```
-┌─────────────┐ ┌──────────────┐ ┌─────────────┐
-│ Browser │◄───────►│ FastAPI │◄───────►│ Discord Bot │
-│ (React UI) │ HTTP/WS │ Backend │ IPC │ (Python) │
-└─────────────┘ └──────────────┘ └─────────────┘
- │
- ▼
- ┌──────────────┐
- │ Database │
- │ (SQLite) │
- └──────────────┘
-```
-
----
-
-## Communication Flow
-
-### 1. Bot → Web (Status Updates)
-Discord bot sends real-time updates to web backend via:
-- **Shared Database** (simplest) - Bot writes to DB, web reads
-- **Redis Pub/Sub** (better) - Bot publishes events, web subscribes
-- **WebSocket/Socket.IO** (best) - Direct real-time connection
-
-### 2. Web → Bot (Commands)
-Web backend sends commands to bot via:
-- **Database flags** (simplest) - Web writes commands, bot polls
-- **Redis Queue** (better) - Web publishes, bot consumes
-- **Direct IPC** (best) - Web calls bot functions directly
-
----
-
-## Detailed Implementation
-
-### Phase 1: Database-Based (Easiest Start)
-
-**How it works:**
-1. Bot writes current state to database
-2. Web reads database and displays
-3. Web writes commands to "commands" table
-4. Bot polls table every second
-
-**Pros:**
-- Simple to implement
-- No new dependencies
-- Works immediately
-
-**Cons:**
-- Not truly real-time (polling delay)
-- Database writes on every update
-
-### Phase 2: Redis-Based (Production Ready)
-
-**How it works:**
-1. Bot publishes events to Redis: `PUBLISH bot:status {"song": "...", "queue": [...]}`
-2. Web subscribes to Redis channel
-3. Web publishes commands: `PUBLISH bot:commands {"action": "skip"}`
-4. Bot subscribes and executes
-
-**Pros:**
-- True real-time
-- Fast and efficient
-- Decoupled architecture
-
-**Cons:**
-- Requires Redis server
-- More complex setup
-
----
-
-## API Endpoints
-
-### GET Endpoints (Read)
-```
-GET /api/servers # List all servers bot is in
-GET /api/servers/{id}/queue # Get current queue
-GET /api/servers/{id}/status # Get playback status
-GET /api/servers/{id}/settings # Get volume/loop/effect
-```
-
-### POST Endpoints (Write)
-```
-POST /api/servers/{id}/play # Add song to queue
-POST /api/servers/{id}/skip # Skip current song
-POST /api/servers/{id}/volume # Set volume
-POST /api/servers/{id}/loop # Set loop mode
-POST /api/servers/{id}/effect # Set audio effect
-POST /api/servers/{id}/shuffle # Shuffle queue
-POST /api/servers/{id}/clear # Clear queue
-```
-
-### WebSocket Events
-```
-ws://localhost:8000/ws/{server_id}
-
-# Bot → Web
-{"event": "song_changed", "data": {...}}
-{"event": "queue_updated", "data": [...]}
-{"event": "volume_changed", "data": 150}
-
-# Web → Bot
-{"action": "skip"}
-{"action": "volume", "value": 120}
-```
-
----
-
-## Example Code Structure
-
-### Backend (FastAPI)
-
-```python
-# main.py
-from fastapi import FastAPI, WebSocket
-from fastapi.middleware.cors import CORSMiddleware
-import asyncio
-import sqlite3
-
-app = FastAPI()
-
-# Allow frontend to connect
-app.add_middleware(
- CORSMiddleware,
- allow_origins=["*"],
- allow_methods=["*"],
- allow_headers=["*"],
-)
-
-# WebSocket connection for real-time updates
-@app.websocket("/ws/{server_id}")
-async def websocket_endpoint(websocket: WebSocket, server_id: str):
- await websocket.accept()
-
- # Send updates every second
- while True:
- # Read from database
- queue_data = get_queue_from_db(server_id)
- await websocket.send_json(queue_data)
- await asyncio.sleep(1)
-
-# API endpoint to skip song
-@app.post("/api/servers/{server_id}/skip")
-async def skip_song(server_id: str):
- # Write command to database
- conn = sqlite3.connect("./data/music.db")
- cursor = conn.cursor()
- cursor.execute("INSERT INTO commands (server_id, action) VALUES (?, ?)",
- (server_id, "skip"))
- conn.commit()
- conn.close()
- return {"status": "ok"}
-```
-
-### Bot Integration
-
-```python
-# In your bot code, add command polling
-@tasks.loop(seconds=1)
-async def process_web_commands():
- """Check for commands from web dashboard"""
- conn = sqlite3.connect("./data/music.db")
- cursor = conn.cursor()
-
- cursor.execute("SELECT * FROM commands WHERE processed = 0")
- commands = cursor.fetchall()
-
- for cmd in commands:
- server_id, action, data = cmd[1], cmd[2], cmd[3]
-
- # Execute command
- if action == "skip":
- guild = bot.get_guild(int(server_id))
- if guild and guild.voice_client:
- guild.voice_client.stop()
-
- # Mark as processed
- cursor.execute("UPDATE commands SET processed = 1 WHERE id = ?", (cmd[0],))
-
- conn.commit()
- conn.close()
-```
-
-### Frontend (React)
-
-```javascript
-// Dashboard.jsx
-import { useEffect, useState } from 'react';
-
-function Dashboard({ serverId }) {
- const [queue, setQueue] = useState([]);
- const [ws, setWs] = useState(null);
-
- useEffect(() => {
- // Connect to WebSocket
- const websocket = new WebSocket(`ws://localhost:8000/ws/${serverId}`);
-
- websocket.onmessage = (event) => {
- const data = JSON.parse(event.data);
- setQueue(data.queue);
- };
-
- setWs(websocket);
-
- return () => websocket.close();
- }, [serverId]);
-
- const skipSong = async () => {
- await fetch(`http://localhost:8000/api/servers/${serverId}/skip`, {
- method: 'POST',
- });
- };
-
- return (
-
-
Now Playing: {queue[0]?.title}
-
-
-
- {queue.map((song, i) => (
- - {song.title}
- ))}
-
-
- );
-}
-```
-
----
-
-## Authentication (Important!)
-
-**Problem:** Anyone with the URL can control your bot.
-
-**Solutions:**
-
-1. **Discord OAuth2** (Recommended)
- - Users log in with Discord
- - Check if user is in the server
- - Only show servers they're members of
-
-2. **API Keys**
- - Generate unique key per server
- - Server admins share key with trusted users
-
-3. **IP Whitelist**
- - Only allow specific IPs to access
-
----
-
-## Deployment
-
-### Development
-```bash
-# Backend
-cd backend
-uvicorn main:app --reload --port 8000
-
-# Frontend
-cd frontend
-npm run dev
-```
-
-### Production
-```bash
-# Backend (systemd service)
-uvicorn main:app --host 0.0.0.0 --port 8000
-
-# Frontend (build static files)
-npm run build
-# Serve with nginx or deploy to Vercel/Netlify
-```
-
----
-
-## File Structure
-
-```
-astro-bot/
-├── bot.py # Discord bot
-├── cogs/
-│ └── music/
-│ ├── main.py
-│ ├── queue.py
-│ └── util.py
-├── web/
-│ ├── backend/
-│ │ ├── main.py # FastAPI app
-│ │ ├── routes/
-│ │ │ ├── servers.py
-│ │ │ └── playback.py
-│ │ └── websockets.py
-│ └── frontend/
-│ ├── src/
-│ │ ├── components/
-│ │ │ ├── Queue.jsx
-│ │ │ ├── Controls.jsx
-│ │ │ └── NowPlaying.jsx
-│ │ ├── App.jsx
-│ │ └── main.jsx
-│ └── package.json
-└── data/
- └── music.db
-```
-
----
-
-## Next Steps
-
-1. **Start with database-based approach** - Get it working first
-2. **Add WebSocket for real-time** - Once basic functionality works
-3. **Build simple UI** - Focus on core features (play, queue, skip)
-4. **Add authentication** - Discord OAuth2
-5. **Polish and deploy** - Make it production-ready
--
2.49.1
From 0572bc090d997986f8f010fbe1a6188fc2b6719f Mon Sep 17 00:00:00 2001
From: Top1055 <123alexfeetham@gmail.com>
Date: Sat, 29 Nov 2025 17:26:26 +0000
Subject: [PATCH 2/2] fixed earrape effect, ffmpeg variables limited. /playfile
now supports file attachments creating a hybrid command
---
cogs/music/main.py | 50 ++++++++++++++++++++++++++++++---------------
cogs/music/queue.py | 3 ++-
2 files changed, 36 insertions(+), 17 deletions(-)
diff --git a/cogs/music/main.py b/cogs/music/main.py
index c3c4c5d..0a5d045 100644
--- a/cogs/music/main.py
+++ b/cogs/music/main.py
@@ -141,37 +141,54 @@ class music(commands.Cog):
await queue.play(ctx)
- @commands.command(
- help="Upload and play an audio file (MP3, MP4, WAV, etc.)",
+ @commands.hybrid_command(
+ name="playfile",
+ description="Upload and play an audio file (MP3, MP4, WAV, etc.)",
aliases=['pf', 'file'])
- async def playfile(self, ctx: Context):
+ @app_commands.describe(file="Audio file to play (MP3, MP4, WAV, OGG, FLAC, etc.)")
+ async def playfile(self, ctx: Context, file: discord.Attachment = None):
"""Play an uploaded audio file from Discord attachment"""
if ctx.guild is None:
- await ctx.send("❌ This command must be used in a server!")
- return
-
- # Check if there's an attachment
- if not ctx.message.attachments:
- await ctx.send(
- "❌ No file attached! Please upload an audio file with your message.\n"
- "**Supported formats:** MP3, MP4, WAV, OGG, FLAC, M4A, WEBM, AAC, OPUS"
- )
+ await ctx.send("❌ This command must be used in a server!", ephemeral=True)
return
server = ctx.guild.id
- attachment = ctx.message.attachments[0]
+
+ # Handle both slash command (file parameter) and prefix command (attachment)
+ if file is not None:
+ # Slash command with file parameter
+ attachment = file
+ elif ctx.message and ctx.message.attachments:
+ # Prefix command with attached file
+ attachment = ctx.message.attachments[0]
+ else:
+ # No file provided
+ await ctx.send(
+ "❌ No file attached! Please upload an audio file.\n"
+ "**Supported formats:** MP3, MP4, WAV, OGG, FLAC, M4A, WEBM, AAC, OPUS",
+ ephemeral=True if ctx.interaction else False
+ )
+ return
# Validate file extension
audio_extensions = ('.mp3', '.mp4', '.wav', '.ogg', '.flac', '.m4a', '.webm', '.aac', '.opus')
if not any(attachment.filename.lower().endswith(ext) for ext in audio_extensions):
await ctx.send(
f"❌ Invalid file type: `{attachment.filename}`\n"
- f"**Supported formats:** MP3, MP4, WAV, OGG, FLAC, M4A, WEBM, AAC, OPUS"
+ f"**Supported formats:** MP3, MP4, WAV, OGG, FLAC, M4A, WEBM, AAC, OPUS",
+ ephemeral=True if ctx.interaction else False
)
return
+ # Defer for slash commands since processing takes time
+ if ctx.interaction:
+ await ctx.defer()
+
await util.join_vc(ctx)
- await ctx.message.add_reaction('📎')
+
+ # Add reaction for prefix commands only
+ if not ctx.interaction:
+ await ctx.message.add_reaction('📎')
msg = await ctx.send(f"Processing file: `{attachment.filename}`...")
@@ -184,7 +201,8 @@ class music(commands.Cog):
await msg.delete()
if len(audio) == 0:
- await ctx.message.add_reaction('🚫')
+ if not ctx.interaction:
+ await ctx.message.add_reaction('🚫')
await ctx.send("❌ Failed to process the audio file!")
return
diff --git a/cogs/music/queue.py b/cogs/music/queue.py
index 96eac30..0a420e0 100644
--- a/cogs/music/queue.py
+++ b/cogs/music/queue.py
@@ -39,7 +39,8 @@ def get_effect_options(effect_name):
'earrape': {
**BASE_FFMPEG_OPTS,
# Aggressive compression + hard clipping + bitcrushing for maximum distortion
- 'options': '-vn -af "volume=8,acompressor=threshold=0.001:ratio=30:attack=0.1:release=5,acrusher=bits=8:mix=0.7,volume=2,alimiter=limit=0.8"'
+ # Note: FFmpeg's acompressor ratio max is 20
+ 'options': '-vn -af "volume=8,acompressor=threshold=0.001:ratio=20:attack=0.1:release=5,acrusher=bits=8:mix=0.7,volume=2,alimiter=limit=0.8"'
},
'deepfry': {
**BASE_FFMPEG_OPTS,
--
2.49.1