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..0a5d045 100644 --- a/cogs/music/main.py +++ b/cogs/music/main.py @@ -141,6 +141,90 @@ class music(commands.Cog): await queue.play(ctx) + @commands.hybrid_command( + name="playfile", + description="Upload and play an audio file (MP3, MP4, WAV, etc.)", + aliases=['pf', 'file']) + @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!", ephemeral=True) + return + + server = ctx.guild.id + + # 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", + 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) + + # Add reaction for prefix commands only + if not ctx.interaction: + 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: + if not ctx.interaction: + 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 +431,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..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, @@ -497,7 +498,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}

- - - -
- ); -} -``` - ---- - -## 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