import discord from discord import app_commands from discord.ext import commands import asyncio import os from dotenv import load_dotenv from ytdl import YTDLSource # Load environment variables load_dotenv() TOKEN = os.getenv('DISCORD_TOKEN') GUILD_ID = os.getenv('GUILD_ID') class MusicBot(commands.Cog): def __init__(self, bot): self.bot = bot self.queues = {} # guild_id -> list self.currents = {} # guild_id -> player self.volumes = {} # guild_id -> volume float self.loop_currents = {} # guild_id -> bool self.last_channels = {} # guild_id -> text channel self.on_state_change_callback = None def get_queue(self, guild_id: int): if guild_id not in self.queues: self.queues[guild_id] = [] return self.queues[guild_id] def get_current(self, guild_id: int): return self.currents.get(guild_id) def set_current(self, guild_id: int, player): self.currents[guild_id] = player def get_volume(self, guild_id: int): return self.volumes.get(guild_id, 1.0) def set_volume(self, guild_id: int, volume: float): self.volumes[guild_id] = volume def get_loop_current(self, guild_id: int): return self.loop_currents.get(guild_id, False) def set_loop_current(self, guild_id: int, loop: bool): self.loop_currents[guild_id] = loop async def trigger_state_change(self): if self.on_state_change_callback: try: if asyncio.iscoroutinefunction(self.on_state_change_callback): await self.on_state_change_callback() else: self.on_state_change_callback() except Exception as e: print(f"Error executing on_state_change_callback: {e}") def load_saved_channel_id(self, guild_id: int): config_path = "config/config.json" if os.path.exists(config_path): try: import json with open(config_path, "r") as f: data = json.load(f) val = data.get(str(guild_id)) return int(val) if val is not None else None except Exception as e: print(f"Error loading config: {e}") return None def save_channel_id(self, guild_id: int, channel_id: int): config_path = "config/config.json" try: import json os.makedirs(os.path.dirname(config_path), exist_ok=True) data = {} if os.path.exists(config_path): with open(config_path, "r") as f: data = json.load(f) data[str(guild_id)] = channel_id with open(config_path, "w") as f: json.dump(data, f, indent=4) except Exception as e: print(f"Error saving config: {e}") def get_notification_channel(self, guild_id: int): if self.last_channels.get(guild_id): return self.last_channels[guild_id] saved_id = self.load_saved_channel_id(guild_id) if saved_id: channel = self.bot.get_channel(int(saved_id)) if channel: self.last_channels[guild_id] = channel return channel # Fallback to the first accessible channel in the guild guild = self.bot.get_guild(guild_id) if guild: for tc in guild.text_channels: if tc.permissions_for(guild.me).send_messages: self.last_channels[guild_id] = tc return tc return None async def ensure_voice(self, interaction): if not interaction.user.voice: await interaction.response.send_message( "Nie jesteś połączony z kanałem głosowym.", ephemeral=True ) return False if interaction.guild.voice_client is None: await interaction.user.voice.channel.connect() elif interaction.guild.voice_client.channel != interaction.user.voice.channel: await interaction.response.send_message( "Jestem już połączony z innym kanałem głosowym.", ephemeral=True ) return False return True @app_commands.command(name="play", description="Odtwórz utwór z YouTube") @app_commands.describe(query="Link YouTube lub zapytanie wyszukiwania") async def play(self, interaction: discord.Interaction, query: str): """Odtwórz utwór z YouTube""" if not await self.ensure_voice(interaction): return await interaction.response.defer() guild_id = interaction.guild.id self.last_channels[guild_id] = interaction.channel try: player = await YTDLSource.from_url(query, loop=self.bot.loop, stream=True) if interaction.guild.voice_client.is_playing(): self.get_queue(guild_id).append(player) await self.trigger_state_change() await interaction.followup.send( f"Dodano do kolejki: {discord.utils.escape_markdown(player.title)}", ephemeral=False ) else: self.set_current(guild_id, player) await self.trigger_state_change() interaction.guild.voice_client.play( player, after=lambda e: asyncio.run_coroutine_threadsafe( self.play_next(guild_id), self.bot.loop ) ) await interaction.followup.send( f"Teraz odtwarzane: {discord.utils.escape_markdown(player.title)}", ephemeral=False ) except Exception as e: await interaction.followup.send( f"Wystąpił błąd: {e}", ephemeral=True ) async def play_next(self, guild_id: int): voice_client = discord.utils.get(self.bot.voice_clients, guild__id=guild_id) if not voice_client: self.set_current(guild_id, None) return loop_current = self.get_loop_current(guild_id) current = self.get_current(guild_id) if loop_current and current: try: url = current.data.get('webpage_url') or current.url player = await YTDLSource.from_url(url, loop=self.bot.loop, stream=True) self.set_current(guild_id, player) await self.trigger_state_change() voice_client.play( player, after=lambda e: asyncio.run_coroutine_threadsafe( self.play_next(guild_id), self.bot.loop ) ) await self.send_to_last_channel(guild_id, f"Teraz odtwarzane (zapętlone): {discord.utils.escape_markdown(player.title)}") return except Exception as e: print(f"Error repeating song: {e}") queue = self.get_queue(guild_id) if len(queue) > 0: next_player = queue.pop(0) self.set_current(guild_id, next_player) await self.trigger_state_change() voice_client.play( next_player, after=lambda e: asyncio.run_coroutine_threadsafe( self.play_next(guild_id), self.bot.loop ) ) await self.send_to_last_channel(guild_id, f"Teraz odtwarzane: {discord.utils.escape_markdown(next_player.title)}") else: self.set_current(guild_id, None) await self.trigger_state_change() await self.send_to_last_channel(guild_id, "Kolejka jest pusta.") async def send_to_last_channel(self, guild_id: int, text: str = None, embed: discord.Embed = None): channel = self.get_notification_channel(guild_id) if channel: try: await channel.send(content=text if text else None, embed=embed) except Exception as e: print(f"Failed to send message: {e}") @app_commands.command(name="queue", description="Pokaż aktualną kolejkę") async def show_queue(self, interaction: discord.Interaction): """Pokaż aktualną kolejkę""" guild_id = interaction.guild.id queue = self.get_queue(guild_id) if not queue: await interaction.response.send_message( "Kolejka jest pusta.", ephemeral=True ) return queue_list = "\n".join([f"{i + 1}. {discord.utils.escape_markdown(item.title)}" for i, item in enumerate(queue)]) await interaction.response.send_message( f"Aktualna kolejka:\n{queue_list}", ephemeral=False ) @app_commands.command(name="skip", description="Pomiń aktualny utwór") async def skip(self, interaction: discord.Interaction): """Pomiń aktualny utwór""" if interaction.guild.voice_client.is_playing(): interaction.guild.voice_client.stop() await self.trigger_state_change() await interaction.response.send_message( "Pominięto aktualny utwór.", ephemeral=False ) else: await interaction.response.send_message( "Nic nie jest odtwarzane.", ephemeral=True ) @app_commands.command(name="pause", description="Wstrzymaj odtwarzanie") async def pause(self, interaction: discord.Interaction): """Wstrzymaj odtwarzanie""" if interaction.guild.voice_client.is_playing(): interaction.guild.voice_client.pause() await self.trigger_state_change() await interaction.response.send_message( "Odtwarzanie wstrzymane.", ephemeral=False ) else: await interaction.response.send_message( "Nic nie jest odtwarzane.", ephemeral=True ) @app_commands.command(name="resume", description="Wznów odtwarzanie") async def resume(self, interaction: discord.Interaction): """Wznów odtwarzanie""" if interaction.guild.voice_client.is_paused(): interaction.guild.voice_client.resume() await self.trigger_state_change() await interaction.response.send_message( "Odtwarzanie wznowione.", ephemeral=False ) else: await interaction.response.send_message( "Odtwarzanie nie jest wstrzymane.", ephemeral=True ) @app_commands.command(name="stop", description="Zatrzymaj odtwarzanie i wyczyść kolejkę") async def stop(self, interaction: discord.Interaction): """Zatrzymaj odtwarzanie i wyczyść kolejkę""" guild_id = interaction.guild.id if interaction.guild.voice_client.is_playing(): interaction.guild.voice_client.stop() self.get_queue(guild_id).clear() self.set_current(guild_id, None) await self.trigger_state_change() await interaction.response.send_message( "Zatrzymano odtwarzanie i wyczyszczono kolejkę.", ephemeral=False ) @app_commands.command(name="volume", description="Zmień głośność odtwarzacza") @app_commands.describe(volume="Poziom głośności (1-100)") async def volume(self, interaction: discord.Interaction, volume: int): """Zmień głośność odtwarzacza""" guild_id = interaction.guild.id if interaction.guild.voice_client is None: return await interaction.response.send_message( "Nie połączono z kanałem głosowym.", ephemeral=True ) if 0 < volume <= 100: self.set_volume(guild_id, volume / 100) interaction.guild.voice_client.source.volume = self.get_volume(guild_id) await self.trigger_state_change() await interaction.response.send_message( f"Zmieniono głośność na {volume}%", ephemeral=False ) return None else: await interaction.response.send_message( "Wprowadź wartość z przedziału od 1 do 100.", ephemeral=True ) return None @app_commands.command(name="nowplaying", description="Pokaż aktualnie odtwarzany utwór") async def now_playing(self, interaction: discord.Interaction): """Pokaż aktualnie odtwarzany utwór""" guild_id = interaction.guild.id current = self.get_current(guild_id) if current: await interaction.response.send_message( f"Teraz odtwarzane: {discord.utils.escape_markdown(current.title)}", ephemeral=False ) else: await interaction.response.send_message( "Nic nie jest obecnie odtwarzane.", ephemeral=True ) @app_commands.command(name="leave", description="Odłącz bota od kanału głosowego") async def leave(self, interaction: discord.Interaction): """Odłącz bota od kanału głosowego""" guild_id = interaction.guild.id if interaction.guild.voice_client is not None: await interaction.guild.voice_client.disconnect() self.get_queue(guild_id).clear() self.set_current(guild_id, None) await interaction.response.send_message( "Rozłączono z kanału głosowego.", ephemeral=False ) else: await interaction.response.send_message( "Nie jestem na kanale głosowym.", ephemeral=True ) @app_commands.command(name="loop", description="Włącz/wyłącz zapętlanie aktualnego utworu") async def loop_command(self, interaction: discord.Interaction): """Włącz/wyłącz zapętlanie aktualnego utworu""" guild_id = interaction.guild.id loop_current = not self.get_loop_current(guild_id) self.set_loop_current(guild_id, loop_current) state = "włączone" if loop_current else "wyłączone" await interaction.response.send_message( f"Zapętlanie aktualnego utworu zostało {state}.", ephemeral=False ) class MyBot(commands.Bot): def __init__(self): intents = discord.Intents.default() intents.message_content = True intents.voice_states = True super().__init__(command_prefix="!", intents=intents) async def setup_hook(self): await self.add_cog(MusicBot(self)) if GUILD_ID: guild = discord.Object(id=int(GUILD_ID)) self.tree.copy_global_to(guild=guild) await self.tree.sync(guild=guild) print(f"Synced commands to guild {GUILD_ID}") else: await self.tree.sync() print("Synced commands globally") async def on_ready(self): print(f'Logged in as {self.user.name} ({self.user.id})') print('------') bot = MyBot()