quick-wins #2
849
PRODUCTION_ROADMAP.md
Normal file
849
PRODUCTION_ROADMAP.md
Normal file
@@ -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
|
||||||
|
<!-- templates/dashboard.html -->
|
||||||
|
<div class="container">
|
||||||
|
<!-- Server Selector -->
|
||||||
|
<select hx-get="/api/servers/{value}/queue"
|
||||||
|
hx-target="#queue-container"
|
||||||
|
hx-trigger="change">
|
||||||
|
{% for server in user_servers %}
|
||||||
|
<option value="{{ server.id }}">{{ server.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- Now Playing (auto-updates every 5s) -->
|
||||||
|
<div id="now-playing"
|
||||||
|
hx-get="/api/now-playing"
|
||||||
|
hx-trigger="every 5s">
|
||||||
|
<!-- Server renders this -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Queue (auto-updates) -->
|
||||||
|
<div id="queue-container"
|
||||||
|
hx-get="/api/queue"
|
||||||
|
hx-trigger="every 3s">
|
||||||
|
<!-- Queue items here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Controls -->
|
||||||
|
<div class="controls">
|
||||||
|
<button hx-post="/api/skip"
|
||||||
|
hx-target="#queue-container">
|
||||||
|
⏭️ Skip
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<input type="range"
|
||||||
|
min="0" max="200"
|
||||||
|
hx-post="/api/volume"
|
||||||
|
hx-trigger="change"
|
||||||
|
hx-vals='{"volume": this.value}'>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Song Form -->
|
||||||
|
<form hx-post="/api/play"
|
||||||
|
hx-target="#queue-container">
|
||||||
|
<input name="query" placeholder="YouTube URL or search">
|
||||||
|
<button type="submit">Add to Queue</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**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! 🎵⏱️
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
# serenity-bot
|
|
||||||
A music bot, will replace my groovy zilean bot and hopefully be written slightly more clearer
|
|
||||||
|
|||||||
@@ -141,6 +141,72 @@ class music(commands.Cog):
|
|||||||
await queue.play(ctx)
|
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(
|
@commands.command(
|
||||||
help="Queue a song to play next (top of queue)",
|
help="Queue a song to play next (top of queue)",
|
||||||
aliases=['pt', 'pn', 'playnext'])
|
aliases=['pt', 'pn', 'playnext'])
|
||||||
@@ -347,8 +413,9 @@ class music(commands.Cog):
|
|||||||
new_vol = await queue.set_volume(server.id, level)
|
new_vol = await queue.set_volume(server.id, level)
|
||||||
|
|
||||||
# Update the current playing song's volume if something is playing
|
# 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:
|
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
|
# Pick an emoji based on volume
|
||||||
if new_vol == 0:
|
if new_vol == 0:
|
||||||
|
|||||||
@@ -497,7 +497,10 @@ async def play(ctx):
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
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)
|
fx = await get_effect(server_id)
|
||||||
opts = get_effect_options(fx)
|
opts = get_effect_options(fx)
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ async def main(url, sp):
|
|||||||
#url = url.lower()
|
#url = url.lower()
|
||||||
|
|
||||||
# Check if link or search
|
# 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)
|
return await search_song(url)
|
||||||
|
|
||||||
#TODO add better regex or something
|
#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_song = 'watch?v=' in url or 'youtu.be/' in url
|
||||||
youtube_playlist = 'playlist?list=' 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)
|
return await song_download(url)
|
||||||
|
|
||||||
if youtube_playlist:
|
if youtube_playlist:
|
||||||
|
|||||||
@@ -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 (
|
|
||||||
<div>
|
|
||||||
<h1>Now Playing: {queue[0]?.title}</h1>
|
|
||||||
<button onClick={skipSong}>Skip</button>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
{queue.map((song, i) => (
|
|
||||||
<li key={i}>{song.title}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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
|
|
||||||
Reference in New Issue
Block a user