fixed queue skip error, implemented more /commands.

This commit is contained in:
2025-11-27 20:24:51 +00:00
parent a36977b000
commit 7f8f77fb76
4 changed files with 420 additions and 26 deletions

View File

@@ -80,7 +80,8 @@ class music(commands.Cog):
# HYBRID COMMAND - works as both =play and /play # HYBRID COMMAND - works as both =play and /play
@commands.hybrid_command( @commands.hybrid_command(
name="play", name="play",
description="Queue a song to play") description="Queue a song to play",
aliases=['p'])
@app_commands.describe(query="YouTube URL, Spotify link, or search query") @app_commands.describe(query="YouTube URL, Spotify link, or search query")
async def play(self, ctx: Context, *, query: str): async def play(self, ctx: Context, *, query: str):
"""Queues a song into the bot""" """Queues a song into the bot"""
@@ -187,7 +188,8 @@ class music(commands.Cog):
@commands.hybrid_command( @commands.hybrid_command(
name="queue", name="queue",
description="Display the current music queue") description="Display the current music queue",
aliases=['q', 'songs'])
async def queue_cmd(self, ctx: Context): async def queue_cmd(self, ctx: Context):
"""Display the current music queue""" """Display the current music queue"""
server = ctx.guild server = ctx.guild
@@ -211,7 +213,8 @@ class music(commands.Cog):
@commands.hybrid_command( @commands.hybrid_command(
name="skip", name="skip",
description="Skip the current song") description="Skip the current song",
aliases=['s'])
@app_commands.describe(count="Number of songs to skip (default: 1)") @app_commands.describe(count="Number of songs to skip (default: 1)")
async def skip(self, ctx: Context, count: int = 1): async def skip(self, ctx: Context, count: int = 1):
"""Skips the current song that is playing""" """Skips the current song that is playing"""
@@ -251,7 +254,8 @@ class music(commands.Cog):
@commands.hybrid_command( @commands.hybrid_command(
name="loop", name="loop",
description="Toggle loop mode") description="Toggle loop mode",
aliases=['l', 'repeat'])
@app_commands.describe(mode="Loop mode: off, song, or queue") @app_commands.describe(mode="Loop mode: off, song, or queue")
@app_commands.choices(mode=[ @app_commands.choices(mode=[
app_commands.Choice(name="Off", value="off"), app_commands.Choice(name="Off", value="off"),
@@ -318,7 +322,8 @@ class music(commands.Cog):
@commands.hybrid_command( @commands.hybrid_command(
name="volume", name="volume",
description="Set playback volume") description="Set playback volume",
aliases=['vol', 'v'])
@app_commands.describe(level="Volume level (0-200%, default shows current)") @app_commands.describe(level="Volume level (0-200%, default shows current)")
async def volume(self, ctx: Context, level: int = None): async def volume(self, ctx: Context, level: int = None):
"""Set or display the current volume""" """Set or display the current volume"""
@@ -360,7 +365,8 @@ class music(commands.Cog):
@commands.hybrid_command( @commands.hybrid_command(
name="effect", name="effect",
description="Apply audio effects to playback") description="Apply audio effects to playback",
aliases=['fx', 'filter'])
@app_commands.describe(effect_name="The audio effect to apply (leave empty to see list)") @app_commands.describe(effect_name="The audio effect to apply (leave empty to see list)")
async def effect(self, ctx: Context, effect_name: str = None): async def effect(self, ctx: Context, effect_name: str = None):
"""Apply or list audio effects""" """Apply or list audio effects"""

View File

@@ -35,10 +35,20 @@ def get_effect_options(effect_name):
**BASE_FFMPEG_OPTS, **BASE_FFMPEG_OPTS,
'options': '-vn -af "atempo=0.8,asetrate=48000*0.8,aecho=0.8:0.9:1000:0.3"' 'options': '-vn -af "atempo=0.8,asetrate=48000*0.8,aecho=0.8:0.9:1000:0.3"'
}, },
'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"'
},
'deepfry': {
**BASE_FFMPEG_OPTS,
# Extreme bitcrushing + bass boost + compression (meme audio effect)
'options': '-vn -af "acrusher=bits=4:mode=log:aa=1,bass=g=15,acompressor=threshold=0.001:ratio=20,volume=3"'
},
'distortion': { 'distortion': {
**BASE_FFMPEG_OPTS, **BASE_FFMPEG_OPTS,
# Pure bitcrushing distortion # Pure bitcrushing distortion
'options': '-vn -af "acrusher=bits=6:mix=0.9,acompressor=threshold=0.01:ratio=15"' 'options': '-vn -af "acrusher=bits=6:mix=0.9,acompressor=threshold=0.01:ratio=15,volume=2"'
}, },
'reverse': { 'reverse': {
**BASE_FFMPEG_OPTS, **BASE_FFMPEG_OPTS,
@@ -76,6 +86,16 @@ def get_effect_options(effect_name):
**BASE_FFMPEG_OPTS, **BASE_FFMPEG_OPTS,
'options': '-vn -af "aecho=0.8:0.88:60:0.4"' 'options': '-vn -af "aecho=0.8:0.88:60:0.4"'
}, },
'phone': {
**BASE_FFMPEG_OPTS,
# Sounds like a phone call (bandpass filter)
'options': '-vn -af "bandpass=f=1500:width_type=h:w=1000,volume=2"'
},
'megaphone': {
**BASE_FFMPEG_OPTS,
# Megaphone/radio effect
'options': '-vn -af "highpass=f=300,lowpass=f=3000,volume=2,acompressor=threshold=0.1:ratio=8"'
}
} }
return effects.get(effect_name, effects['none']) return effects.get(effect_name, effects['none'])
@@ -202,7 +222,12 @@ async def add_song(server_id, details, queued_by):
# Pop song from server (respects loop mode) # Pop song from server (respects loop mode)
async def pop(server_id, ignore=False): async def pop(server_id, ignore=False, skip_mode=False):
"""
Pop next song from queue
ignore: Skip the song without returning URL
skip_mode: True when called from skip command (affects loop song behavior)
"""
# Connect to db # Connect to db
conn = sqlite3.connect(db_path) conn = sqlite3.connect(db_path)
cursor = conn.cursor() cursor = conn.cursor()
@@ -624,6 +649,8 @@ def get_effect_emoji(effect_name):
'bassboost': '🔉💥', 'bassboost': '🔉💥',
'nightcore': '⚡🎀', 'nightcore': '⚡🎀',
'slowed': '🐌💤', 'slowed': '🐌💤',
'earrape': '💀📢',
'deepfry': '🍟💥',
'distortion': '⚡🔊', 'distortion': '⚡🔊',
'reverse': '⏪🔄', 'reverse': '⏪🔄',
'chipmunk': '🐿️', 'chipmunk': '🐿️',
@@ -634,6 +661,8 @@ def get_effect_emoji(effect_name):
'vibrato': '〰️', 'vibrato': '〰️',
'tremolo': '📳', 'tremolo': '📳',
'echo': '🗣️💭', 'echo': '🗣️💭',
'phone': '📞',
'megaphone': '📢📣'
} }
return emojis.get(effect_name, '🔊') return emojis.get(effect_name, '🔊')
@@ -645,6 +674,8 @@ def get_effect_description(effect_name):
'bassboost': 'MAXIMUM BASS 🔊', 'bassboost': 'MAXIMUM BASS 🔊',
'nightcore': 'Speed + pitch up (anime vibes)', 'nightcore': 'Speed + pitch up (anime vibes)',
'slowed': 'Slowed + reverb (TikTok aesthetic)', 'slowed': 'Slowed + reverb (TikTok aesthetic)',
'earrape': '⚠️ Aggressive compression + distortion + clipping ⚠️',
'deepfry': '🍟 EXTREME bitcrushing + bass (meme audio) 🍟',
'distortion': 'Heavy bitcrushing distortion', 'distortion': 'Heavy bitcrushing distortion',
'reverse': 'Plays audio BACKWARDS', 'reverse': 'Plays audio BACKWARDS',
'chipmunk': 'High pitched and fast (Alvin mode)', 'chipmunk': 'High pitched and fast (Alvin mode)',
@@ -655,6 +686,8 @@ def get_effect_description(effect_name):
'vibrato': 'Warbling pitch effect', 'vibrato': 'Warbling pitch effect',
'tremolo': 'Volume oscillation', 'tremolo': 'Volume oscillation',
'echo': 'Echo/reverb effect', 'echo': 'Echo/reverb effect',
'phone': 'Sounds like a phone call',
'megaphone': 'Megaphone/radio effect'
} }
return descriptions.get(effect_name, 'Unknown effect') return descriptions.get(effect_name, 'Unknown effect')

View File

@@ -211,12 +211,14 @@ async def display_server_queue(ctx: Context, songs, n):
loop_emoji = loop_emojis.get(loop_mode, '') loop_emoji = loop_emojis.get(loop_mode, '')
effect_emoji = queue.get_effect_emoji(effect) effect_emoji = queue.get_effect_emoji(effect)
# Progress bar (━ for filled, ─ for empty) # Progress bar - using Unicode block characters for smooth look
progress_bar = "" progress_bar = ""
if duration > 0: if duration > 0:
bar_length = 15 bar_length = 20 # Increased from 15 for smoother display
filled = int((percentage / 100) * bar_length) filled = int((percentage / 100) * bar_length)
progress_bar = f"\n{'' * filled}{'' * (bar_length - filled)} `{format_time(elapsed)} / {format_time(duration)}`"
# Use block characters: █ for filled, ░ for empty
progress_bar = f"\n{'' * filled}{'' * (bar_length - filled)} `{format_time(elapsed)} / {format_time(duration)}`"
# Now playing section # Now playing section
now_playing = f"### 🔊 Now Playing\n**{current_song}** {loop_emoji}{progress_bar}\n" now_playing = f"### 🔊 Now Playing\n**{current_song}** {loop_emoji}{progress_bar}\n"

353
web_dhasboard_plan.md Normal file
View File

@@ -0,0 +1,353 @@
# 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