From 7f8f77fb760bf21f28b4d1ba20265fde4dc16ad9 Mon Sep 17 00:00:00 2001 From: Top1055 <123alexfeetham@gmail.com> Date: Thu, 27 Nov 2025 20:24:51 +0000 Subject: [PATCH] fixed queue skip error, implemented more /commands. --- cogs/music/main.py | 48 +++--- cogs/music/queue.py | 37 ++++- cogs/music/util.py | 8 +- web_dhasboard_plan.md | 353 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 420 insertions(+), 26 deletions(-) create mode 100644 web_dhasboard_plan.md diff --git a/cogs/music/main.py b/cogs/music/main.py index fa6d272..fe3504a 100644 --- a/cogs/music/main.py +++ b/cogs/music/main.py @@ -68,7 +68,7 @@ class music(commands.Cog): await util.join_vc(ctx) await ctx.message.add_reaction('👍') - + @commands.command( help="Leaves the voice chat if the bot is present", aliases=['disconnect']) @@ -80,7 +80,8 @@ class music(commands.Cog): # HYBRID COMMAND - works as both =play and /play @commands.hybrid_command( 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") async def play(self, ctx: Context, *, query: str): """Queues a song into the bot""" @@ -95,13 +96,13 @@ class music(commands.Cog): await ctx.defer() await util.join_vc(ctx) - + # Different responses for slash vs prefix if not ctx.interaction: await ctx.message.add_reaction('👍') msg = await ctx.send("Fetching song(s)...") - + #TODO potentially save requests before getting stream link # Grab video details such as title thumbnail duration audio = await translate.main(query, self.sp) @@ -187,7 +188,8 @@ class music(commands.Cog): @commands.hybrid_command( name="queue", - description="Display the current music queue") + description="Display the current music queue", + aliases=['q', 'songs']) async def queue_cmd(self, ctx: Context): """Display the current music queue""" server = ctx.guild @@ -208,10 +210,11 @@ class music(commands.Cog): # Display songs await util.display_server_queue(ctx, songs, n) - + @commands.hybrid_command( 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)") async def skip(self, ctx: Context, count: int = 1): """Skips the current song that is playing""" @@ -231,7 +234,7 @@ class music(commands.Cog): # Check loop mode loop_mode = await queue.get_loop_mode(server.id) - + if loop_mode == 'song' and count == 1: # When looping song and skipping once, just restart it ctx.voice_client.stop() @@ -240,18 +243,19 @@ class music(commands.Cog): # Skip specified number of songs for _ in range(count-1): await queue.pop(server.id, True, skip_mode=True) - + # Last song gets skip_mode=True to handle loop properly if loop_mode != 'song': await queue.pop(server.id, True, skip_mode=True) - + ctx.voice_client.stop() await ctx.send(f"⏭️ Skipped {count} song(s)") - + @commands.hybrid_command( 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.choices(mode=[ app_commands.Choice(name="Off", value="off"), @@ -318,7 +322,8 @@ class music(commands.Cog): @commands.hybrid_command( name="volume", - description="Set playback volume") + description="Set playback volume", + aliases=['vol', 'v']) @app_commands.describe(level="Volume level (0-200%, default shows current)") async def volume(self, ctx: Context, level: int = None): """Set or display the current volume""" @@ -360,7 +365,8 @@ class music(commands.Cog): @commands.hybrid_command( 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)") async def effect(self, ctx: Context, effect_name: str = None): """Apply or list audio effects""" @@ -375,16 +381,16 @@ class music(commands.Cog): current = await queue.get_effect(server.id) emoji = queue.get_effect_emoji(current) desc = queue.get_effect_description(current) - + effects_list = '\n'.join([ - f"`{e}` - {queue.get_effect_description(e)}" + f"`{e}` - {queue.get_effect_description(e)}" for e in queue.list_all_effects()[:9] # Show first 9 ]) more_effects = '\n'.join([ - f"`{e}` - {queue.get_effect_description(e)}" + f"`{e}` - {queue.get_effect_description(e)}" for e in queue.list_all_effects()[9:] # Show rest ]) - + await ctx.send( f"{emoji} **Current effect:** {current} - {desc}\n\n" f"**Available effects:**\n{effects_list}\n\n" @@ -395,7 +401,7 @@ class music(commands.Cog): # Set effect effect_name = effect_name.lower() - + if effect_name not in queue.list_all_effects(): await ctx.send( f"❌ Unknown effect! Use `/effect` to see available effects.", @@ -404,10 +410,10 @@ class music(commands.Cog): return await queue.set_effect(server.id, effect_name) - + emoji = queue.get_effect_emoji(effect_name) desc = queue.get_effect_description(effect_name) - + # Special warning for earrape/deepfry if effect_name in ['earrape', 'deepfry']: await ctx.send( diff --git a/cogs/music/queue.py b/cogs/music/queue.py index 9058529..eb34e54 100644 --- a/cogs/music/queue.py +++ b/cogs/music/queue.py @@ -35,10 +35,20 @@ def get_effect_options(effect_name): **BASE_FFMPEG_OPTS, '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': { **BASE_FFMPEG_OPTS, # 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': { **BASE_FFMPEG_OPTS, @@ -76,6 +86,16 @@ def get_effect_options(effect_name): **BASE_FFMPEG_OPTS, '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']) @@ -202,7 +222,12 @@ async def add_song(server_id, details, queued_by): # 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 conn = sqlite3.connect(db_path) cursor = conn.cursor() @@ -624,6 +649,8 @@ def get_effect_emoji(effect_name): 'bassboost': '🔉💥', 'nightcore': '⚡🎀', 'slowed': '🐌💤', + 'earrape': '💀📢', + 'deepfry': '🍟💥', 'distortion': '⚡🔊', 'reverse': '⏪🔄', 'chipmunk': '🐿️', @@ -634,6 +661,8 @@ def get_effect_emoji(effect_name): 'vibrato': '〰️', 'tremolo': '📳', 'echo': '🗣️💭', + 'phone': '📞', + 'megaphone': '📢📣' } return emojis.get(effect_name, '🔊') @@ -645,6 +674,8 @@ def get_effect_description(effect_name): 'bassboost': 'MAXIMUM BASS 🔊', 'nightcore': 'Speed + pitch up (anime vibes)', 'slowed': 'Slowed + reverb (TikTok aesthetic)', + 'earrape': '⚠️ Aggressive compression + distortion + clipping ⚠️', + 'deepfry': '🍟 EXTREME bitcrushing + bass (meme audio) 🍟', 'distortion': 'Heavy bitcrushing distortion', 'reverse': 'Plays audio BACKWARDS', 'chipmunk': 'High pitched and fast (Alvin mode)', @@ -655,6 +686,8 @@ def get_effect_description(effect_name): 'vibrato': 'Warbling pitch effect', 'tremolo': 'Volume oscillation', 'echo': 'Echo/reverb effect', + 'phone': 'Sounds like a phone call', + 'megaphone': 'Megaphone/radio effect' } return descriptions.get(effect_name, 'Unknown effect') diff --git a/cogs/music/util.py b/cogs/music/util.py index caaf117..9942af3 100644 --- a/cogs/music/util.py +++ b/cogs/music/util.py @@ -211,12 +211,14 @@ async def display_server_queue(ctx: Context, songs, n): loop_emoji = loop_emojis.get(loop_mode, '') effect_emoji = queue.get_effect_emoji(effect) - # Progress bar (━ for filled, ─ for empty) + # Progress bar - using Unicode block characters for smooth look progress_bar = "" if duration > 0: - bar_length = 15 + bar_length = 20 # Increased from 15 for smoother display 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 = f"### 🔊 Now Playing\n**{current_song}** {loop_emoji}{progress_bar}\n" diff --git a/web_dhasboard_plan.md b/web_dhasboard_plan.md new file mode 100644 index 0000000..d15293f --- /dev/null +++ b/web_dhasboard_plan.md @@ -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 ( +