from discord.ext import commands from discord.ext.commands.context import Context from discord import app_commands import discord import cogs.music.util as util import cogs.music.queue as queue import cogs.music.translate as translate from cogs.music.help import music_help import spotipy from spotipy.oauth2 import SpotifyClientCredentials import config # Use centralized config class music(commands.Cog): def __init__(self, client): self.client = client self.name = "đŸŽļ Music" self.emoji = "đŸŽļ" help_command = music_help() help_command.cog = self self.help_command = help_command # Get Spotify credentials from centralized config spotify_id, spotify_secret = config.get_spotify_creds() # Authentication - without user client_credentials_manager = SpotifyClientCredentials( client_id=spotify_id, client_secret=spotify_secret ) self.sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) queue.initialize_tables() @commands.command( help="Connects to your current voice channel", aliases=['connect']) async def join(self, ctx: Context): await util.join_vc(ctx) await ctx.message.add_reaction('👍') @commands.command( help="Leaves the voice chat if the bot is present", aliases=['disconnect']) async def leave(self, ctx: Context): await util.leave_vc(ctx) await ctx.message.add_reaction('👍') # HYBRID COMMAND - works as both =play and /play @commands.hybrid_command( name="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""" if ctx.guild is None: await ctx.send("❌ This command must be used in a server!", ephemeral=True) return server = ctx.guild.id # For slash commands, defer the response since fetching takes time if ctx.interaction: 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) await msg.delete() if len(audio) == 0: if not ctx.interaction: await ctx.message.add_reaction('đŸšĢ') await ctx.send("❌ Failed to find song!") return #TODO make sure user isn't queuing in dm for some stupid reason # Setup first song's position audio[0]['position'] = await queue.add_song( server, audio[0], ctx.author.display_name) # Add any other songs for song in audio[1:]: await queue.add_song( server, song, 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.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']) async def playtop(self, ctx: Context, *, url=None): """Queue a song at the top of the queue (plays next)""" if url is None: raise commands.CommandError("Must provide a link or search query") elif ctx.guild is None: raise commands.CommandError("Command must be issued in a server") server = ctx.guild.id await util.join_vc(ctx) await ctx.message.add_reaction('â­ī¸') msg = await ctx.send("Fetching song(s) to play next...") async with ctx.typing(): audio = await translate.main(url, self.sp) await msg.delete() if len(audio) == 0: await ctx.message.add_reaction('đŸšĢ') await ctx.send("Failed to find song!") return # Add songs to TOP of queue (position 0, then 1, then 2...) for i, song in enumerate(audio): await queue.add_song( server, song, ctx.author.display_name, position=i) # Insert at top # Show first song that was added audio[0]['position'] = 0 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.hybrid_command( name="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 # Perform usual checks if server is None: await ctx.send("❌ This command must be used in a server!", ephemeral=True) return # Grab all songs from this server n, songs = await queue.grab_songs(server.id) # Check once more if len(songs) == 0 and await queue.is_server_playing(ctx.guild.id) == False: await ctx.send("đŸšĢ This server has no queue currently. Start the party by queuing up a song!") return # Display songs await util.display_server_queue(ctx, songs, n) @commands.hybrid_command( name="skip", 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""" server = ctx.guild if server is None: await ctx.send("❌ This command must be used in a server!", ephemeral=True) return if ctx.voice_client is None: await ctx.send("❌ I'm not in a voice channel!", ephemeral=True) return if count <= 0: await ctx.send("❌ Please enter a positive number!", ephemeral=True) return # 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() await ctx.send(f"â­ī¸ Restarting song (loop mode active)") else: # 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", aliases=['l', 'repeat']) @app_commands.describe(mode="Loop mode: off, song, or queue") @app_commands.choices(mode=[ app_commands.Choice(name="Off", value="off"), app_commands.Choice(name="Song", value="song"), app_commands.Choice(name="Queue", value="queue") ]) async def loop(self, ctx: Context, mode: str = None): """Toggle between loop modes or set a specific mode""" server = ctx.guild if server is None: await ctx.send("❌ This command must be used in a server!", ephemeral=True) return current_mode = await queue.get_loop_mode(server.id) # If no mode specified, cycle through modes if mode is None: if current_mode == 'off': new_mode = 'song' elif current_mode == 'song': new_mode = 'queue' else: new_mode = 'off' else: # Set specific mode mode = mode.lower() if mode not in ['off', 'song', 'queue']: await ctx.send("❌ Loop mode must be: off, song, or queue", ephemeral=True) return new_mode = mode await queue.set_loop_mode(server.id, new_mode) # Response messages emojis = {'off': 'âšī¸', 'song': '🔂', 'queue': '🔁'} messages = { 'off': 'Loop disabled', 'song': 'Looping current song 🔂 (skip to restart)', 'queue': 'Looping entire queue 🔁' } await ctx.send(f"{emojis[new_mode]} {messages[new_mode]}") @commands.command( help="Shuffle the queue randomly", aliases=['mix', 'randomize']) async def shuffle(self, ctx: Context): """Shuffle all songs in the queue""" server = ctx.guild if server is None: raise commands.CommandError("Command must be issued in a server") success = await queue.shuffle_queue(server.id) if not success: await ctx.send("đŸšĢ Not enough songs in the queue to shuffle!") else: await ctx.message.add_reaction('🔀') await ctx.send("🔀 Queue shuffled!") @commands.hybrid_command( name="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""" server = ctx.guild if server is None: await ctx.send("❌ This command must be used in a server!", ephemeral=True) return if level is None: # Display current volume current_vol = await queue.get_volume(server.id) await ctx.send(f"🔊 Current volume: {current_vol}%") return # Set volume if level < 0 or level > 200: await ctx.send("❌ Volume must be between 0 and 200!", ephemeral=True) return 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 * 0.25 # Pick an emoji based on volume if new_vol == 0: emoji = '🔇' elif new_vol < 50: emoji = '🔉' elif new_vol < 100: emoji = '🔊' else: emoji = 'đŸ“ĸ' await ctx.send(f"{emoji} Volume set to {new_vol}%") @commands.hybrid_command( name="effect", 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""" server = ctx.guild if server is None: await ctx.send("❌ This command must be used in a server!", ephemeral=True) return # If no effect specified, show current effect and list options if effect_name is None: 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)}" for e in queue.list_all_effects()[:9] # Show first 9 ]) more_effects = '\n'.join([ 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" f"**More effects:**\n{more_effects}\n\n" f"Use `=effect ` or `/effect ` to apply!" ) return # 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.", ephemeral=True ) 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( f"âš ī¸ **{effect_name.upper()} MODE ACTIVATED** âš ī¸\n" f"{desc}\n" f"Effect will apply to next song.\n" f"Use `/effect none` to disable." ) else: await ctx.send( f"{emoji} Effect set to **{effect_name}**\n" f"{desc}\n" f"Effect will apply to next song!" ) # If something is currently playing, notify about skip if ctx.voice_client and ctx.voice_client.is_playing(): await ctx.send("💡 Tip: Use `/skip` to apply effect immediately!")