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.history = {} # guild_id -> list of dicts self.on_state_change_callback = None def load_history(self, guild_id: int): if guild_id in self.history: return self.history[guild_id] config_path = "config/history.json" if os.path.exists(config_path): try: import json with open(config_path, "r") as f: data = json.load(f) self.history[guild_id] = data.get(str(guild_id), []) except Exception as e: print(f"Error loading history: {e}") self.history[guild_id] = [] else: self.history[guild_id] = [] return self.history[guild_id] def save_history(self, guild_id: int): config_path = "config/history.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)] = self.history.get(guild_id, []) with open(config_path, "w") as f: json.dump(data, f, indent=4) except Exception as e: print(f"Error saving history: {e}") def add_to_history(self, guild_id: int, player): if not player or not player.title: return history = self.load_history(guild_id) entry = { "id": player.id, "title": player.title, "url": player.data.get("webpage_url") or player.url, "duration": player.duration, "is_local": player.data.get("is_local", False), "filepath": player.data.get("filepath") } history.insert(0, entry) self.history[guild_id] = history[:50] self.save_history(guild_id) 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: filepath = os.path.join("uploads", os.path.basename(query)) if os.path.exists(filepath) and not query.startswith("http"): player = await YTDLSource.from_file(filepath, loop=self.bot.loop) else: 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) self.add_to_history(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 uploads_autocomplete( self, interaction: discord.Interaction, current: str, ) -> list[app_commands.Choice[str]]: uploads_dir = "uploads" if not os.path.exists(uploads_dir): return [] files = [f for f in os.listdir(uploads_dir) if os.path.isfile(os.path.join(uploads_dir, f)) and not f.startswith(".")] filtered = [f for f in files if current.lower() in f.lower()][:25] return [app_commands.Choice(name=f, value=f) for f in filtered] @app_commands.command(name="playfile", description="Odtwórz plik przesłany na serwer") @app_commands.describe(filename="Wybierz plik z listy przesłanych") @app_commands.autocomplete(filename=uploads_autocomplete) async def playfile(self, interaction: discord.Interaction, filename: str): """Odtwórz plik przesłany na serwer""" if not await self.ensure_voice(interaction): return await interaction.response.defer() guild_id = interaction.guild.id self.last_channels[guild_id] = interaction.channel filepath = os.path.join("uploads", os.path.basename(filename)) if not os.path.exists(filepath): await interaction.followup.send("Nie znaleziono pliku na serwerze.", ephemeral=True) return try: player = await YTDLSource.from_file(filepath, loop=self.bot.loop) 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) self.add_to_history(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: if current.data.get('is_local'): player = await YTDLSource.from_file(current.data['filepath'], loop=self.bot.loop, title=current.title) else: 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) self.add_to_history(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 ) async def history_autocomplete( self, interaction: discord.Interaction, current: str, ) -> list[app_commands.Choice[str]]: guild_id = interaction.guild.id history = self.load_history(guild_id) filtered = [h for h in history if current.lower() in h["title"].lower()][:25] return [app_commands.Choice(name=h["title"][:100], value=str(h["id"])) for h in filtered] @app_commands.command(name="history", description="Pokaż historię ostatnio odtwarzanych utworów") async def show_history(self, interaction: discord.Interaction): """Pokaż historię odtwarzania""" guild_id = interaction.guild.id history = self.load_history(guild_id) if not history: await interaction.response.send_message( "Historia odtwarzania jest pusta.", ephemeral=True ) return history_list = "\n".join([f"{i + 1}. {discord.utils.escape_markdown(item['title'])}" for i, item in enumerate(history[:15])]) await interaction.response.send_message( f"📜 Historia odtwarzania (ostatnie 15):\n{history_list}", ephemeral=False ) @app_commands.command(name="replay", description="Odtwórz ponownie utwór z historii") @app_commands.describe(track_id="Wybierz utwór z historii odtwarzania") @app_commands.autocomplete(track_id=history_autocomplete) async def replay(self, interaction: discord.Interaction, track_id: str): """Odtwórz z historii""" if not await self.ensure_voice(interaction): return await interaction.response.defer() guild_id = interaction.guild.id self.last_channels[guild_id] = interaction.channel history = self.load_history(guild_id) entry = next((item for item in history if str(item["id"]) == track_id), None) # If not found by ID, maybe user typed an index number 1..15 if not entry and track_id.isdigit(): idx = int(track_id) - 1 if 0 <= idx < len(history): entry = history[idx] if not entry: await interaction.followup.send("Nie znaleziono utworu w historii.", ephemeral=True) return try: if entry.get("is_local"): filepath = entry.get("filepath") or entry.get("url") if not os.path.exists(filepath): await interaction.followup.send("Plik lokalny z historii już nie istnieje na serwerze.", ephemeral=True) return player = await YTDLSource.from_file(filepath, loop=self.bot.loop, title=entry["title"]) else: player = await YTDLSource.from_url(entry["url"], 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 z historii: {discord.utils.escape_markdown(player.title)}", ephemeral=False ) else: self.set_current(guild_id, player) self.add_to_history(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 z historii: {discord.utils.escape_markdown(player.title)}", ephemeral=False ) except Exception as e: await interaction.followup.send(f"Wystąpił błąd: {e}", ephemeral=True) @app_commands.command(name="skip", description="Pomiń aktualny utwór") async def skip(self, interaction: discord.Interaction): """Pomiń aktualny utwór""" guild_id = interaction.guild.id if self.get_loop_current(guild_id): self.set_loop_current(guild_id, False) if interaction.guild.voice_client and (interaction.guild.voice_client.is_playing() or interaction.guild.voice_client.is_paused()): 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()