From 2dcaaf229006693635678864e0db358079eabd04 Mon Sep 17 00:00:00 2001 From: streloss Date: Wed, 11 Mar 2026 22:51:17 +0900 Subject: [PATCH] Add files via upload --- cogs/music.py | 167 +++++++++++++++++++++++++++++++++++++++++++++++++ cogs/player.py | 87 ++++++++++++++++++++++++++ cogs/queue.py | 70 +++++++++++++++++++++ 3 files changed, 324 insertions(+) create mode 100644 cogs/music.py create mode 100644 cogs/player.py create mode 100644 cogs/queue.py diff --git a/cogs/music.py b/cogs/music.py new file mode 100644 index 0000000..f38db09 --- /dev/null +++ b/cogs/music.py @@ -0,0 +1,167 @@ +import discord +from discord.ext import commands +from discord import app_commands +import asyncio + +from cogs.player import ( + YTDLSource, get_player, + now_playing_embed, fmt_duration +) + +class Music(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + + # ── Внутренний метод: следующий трек ────── + async def play_next(self, guild: discord.Guild, channel: discord.TextChannel): + player = get_player(guild.id) + vc: discord.VoiceClient = guild.voice_client + + if not vc: + return + + # Повтор текущего трека + if player.loop and player.current: + try: + source = await YTDLSource.from_url(player.current.url, loop=self.bot.loop) + source.volume = player.volume + player.current = source + vc.play(source, after=lambda e: asyncio.run_coroutine_threadsafe( + self.play_next(guild, channel), self.bot.loop)) + await channel.send(embed=now_playing_embed(source)) + except Exception as e: + await channel.send(f"❌ Ошибка повтора: {e}") + return + + if player.queue: + url = player.queue.popleft() + try: + source = await YTDLSource.from_url(url, loop=self.bot.loop) + except Exception as e: + await channel.send(f"❌ Ошибка воспроизведения: {e}") + return + source.volume = player.volume + player.current = source + vc.play(source, after=lambda e: asyncio.run_coroutine_threadsafe( + self.play_next(guild, channel), self.bot.loop)) + await channel.send(embed=now_playing_embed(source)) + else: + player.current = None + await channel.send("✅ Очередь закончилась.") + + # ── /play ───────────────────────────────── + @app_commands.command(name="play", description="Воспроизвести трек по названию или ссылке") + @app_commands.describe(query="Название песни или YouTube ссылка") + async def play(self, interaction: discord.Interaction, query: str): + await interaction.response.defer() + + if not interaction.user.voice: + return await interaction.followup.send("❌ Сначала зайди в голосовой канал!") + + vc: discord.VoiceClient = interaction.guild.voice_client + if not vc: + vc = await interaction.user.voice.channel.connect() + elif interaction.user.voice.channel != vc.channel: + await vc.move_to(interaction.user.voice.channel) + + player = get_player(interaction.guild.id) + + try: + source = await YTDLSource.from_url(query, loop=self.bot.loop) + except Exception as e: + return await interaction.followup.send(f"❌ Не удалось найти трек: {e}") + + source.volume = player.volume + + if vc.is_playing() or vc.is_paused(): + player.queue.append(source.url) + embed = discord.Embed( + title="📥 Добавлено в очередь", + description=f"**{source.title}**", + color=0x5865F2 + ) + embed.add_field(name="Позиция", value=str(len(player.queue))) + return await interaction.followup.send(embed=embed) + + player.current = source + vc.play(source, after=lambda e: asyncio.run_coroutine_threadsafe( + self.play_next(interaction.guild, interaction.channel), self.bot.loop)) + await interaction.followup.send(embed=now_playing_embed(source)) + + # ── /skip ───────────────────────────────── + @app_commands.command(name="skip", description="Пропустить текущий трек") + async def skip(self, interaction: discord.Interaction): + vc = interaction.guild.voice_client + if vc and vc.is_playing(): + vc.stop() + await interaction.response.send_message("⏭️ Трек пропущен.") + else: + await interaction.response.send_message("❌ Ничего не играет.") + + # ── /pause ──────────────────────────────── + @app_commands.command(name="pause", description="Пауза / продолжить") + async def pause(self, interaction: discord.Interaction): + vc = interaction.guild.voice_client + if vc and vc.is_playing(): + vc.pause() + await interaction.response.send_message("⏸️ Пауза.") + elif vc and vc.is_paused(): + vc.resume() + await interaction.response.send_message("▶️ Продолжаю.") + else: + await interaction.response.send_message("❌ Ничего не играет.") + + # ── /stop ───────────────────────────────── + @app_commands.command(name="stop", description="Остановить музыку и очистить очередь") + async def stop(self, interaction: discord.Interaction): + player = get_player(interaction.guild.id) + player.queue.clear() + player.current = None + vc = interaction.guild.voice_client + if vc: + vc.stop() + await vc.disconnect() + await interaction.response.send_message("⏹️ Остановлено, очередь очищена.") + + # ── /loop ───────────────────────────────── + @app_commands.command(name="loop", description="Включить/выключить повтор трека") + async def loop(self, interaction: discord.Interaction): + player = get_player(interaction.guild.id) + player.loop = not player.loop + status = "включён 🔁" if player.loop else "выключен ➡️" + await interaction.response.send_message(f"Повтор {status}") + + # ── /nowplaying ─────────────────────────── + @app_commands.command(name="nowplaying", description="Текущий трек") + async def nowplaying(self, interaction: discord.Interaction): + player = get_player(interaction.guild.id) + if player.current: + await interaction.response.send_message(embed=now_playing_embed(player.current)) + else: + await interaction.response.send_message("❌ Ничего не играет.") + + # ── /volume ─────────────────────────────── + @app_commands.command(name="volume", description="Громкость от 1 до 100") + @app_commands.describe(level="Уровень громкости (1–100)") + async def volume(self, interaction: discord.Interaction, level: int): + if not 1 <= level <= 100: + return await interaction.response.send_message("❌ Укажи значение от 1 до 100.") + player = get_player(interaction.guild.id) + player.volume = level / 100 + vc = interaction.guild.voice_client + if vc and vc.source: + vc.source.volume = player.volume + await interaction.response.send_message(f"🔊 Громкость: **{level}%**") + + # ── /leave ──────────────────────────────── + @app_commands.command(name="leave", description="Выгнать бота из канала") + async def leave(self, interaction: discord.Interaction): + vc = interaction.guild.voice_client + if vc: + await vc.disconnect() + await interaction.response.send_message("👋 Отключился.") + else: + await interaction.response.send_message("❌ Я не в канале.") + +async def setup(bot: commands.Bot): + await bot.add_cog(Music(bot)) \ No newline at end of file diff --git a/cogs/player.py b/cogs/player.py new file mode 100644 index 0000000..5aca41a --- /dev/null +++ b/cogs/player.py @@ -0,0 +1,87 @@ +import discord +import yt_dlp +import asyncio +from collections import deque + +# ───────────────────────────────────────────── +# Конфиг yt-dlp +# ───────────────────────────────────────────── +YTDL_OPTIONS = { + "format": "bestaudio/best", + "noplaylist": True, + "quiet": True, + "no_warnings": True, + "default_search": "ytsearch", + "source_address": "0.0.0.0", +} + +FFMPEG_OPTIONS = { + "before_options": "-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5", + "options": "-vn -bufsize 512k", +} + +ytdl = yt_dlp.YoutubeDL(YTDL_OPTIONS) + +# ───────────────────────────────────────────── +# Источник аудио +# ───────────────────────────────────────────── +class YTDLSource(discord.PCMVolumeTransformer): + def __init__(self, source, *, data, volume=0.5): + super().__init__(source, volume) + self.data = data + self.title = data.get("title", "Неизвестно") + self.url = data.get("webpage_url", "") + self.duration = data.get("duration", 0) + self.thumbnail = data.get("thumbnail", "") + + @classmethod + async def from_url(cls, url: str, *, loop=None): + loop = loop or asyncio.get_event_loop() + data = await loop.run_in_executor( + None, + lambda: ytdl.extract_info(url, download=False) + ) + if "entries" in data: + data = data["entries"][0] + return cls( + discord.FFmpegPCMAudio(data["url"], **FFMPEG_OPTIONS), + data=data + ) + +# ───────────────────────────────────────────── +# Состояние плеера (на сервер) +# ───────────────────────────────────────────── +class GuildPlayer: + def __init__(self): + self.queue: deque = deque() + self.current: YTDLSource | None = None + self.volume: float = 0.5 + self.loop: bool = False + +# Глобальный словарь плееров +players: dict[int, GuildPlayer] = {} + +def get_player(guild_id: int) -> GuildPlayer: + if guild_id not in players: + players[guild_id] = GuildPlayer() + return players[guild_id] + +# ───────────────────────────────────────────── +# Хелперы +# ───────────────────────────────────────────── +def fmt_duration(sec: int) -> str: + m, s = divmod(sec, 60) + h, m = divmod(m, 60) + return f"{h}:{m:02d}:{s:02d}" if h else f"{m}:{s:02d}" + +def now_playing_embed(source: YTDLSource) -> discord.Embed: + embed = discord.Embed( + title="🎵 Сейчас играет", + description=f"**[{source.title}]({source.url})**", + color=0x1DB954 + ) + if source.duration: + embed.add_field(name="Длительность", value=fmt_duration(source.duration)) + if source.thumbnail: + embed.set_thumbnail(url=source.thumbnail) + return embed \ No newline at end of file diff --git a/cogs/queue.py b/cogs/queue.py new file mode 100644 index 0000000..2a4dbb3 --- /dev/null +++ b/cogs/queue.py @@ -0,0 +1,70 @@ +import discord +from discord.ext import commands +from discord import app_commands + +from cogs.player import get_player, fmt_duration + +class Queue(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + + # ── /queue ──────────────────────────────── + @app_commands.command(name="queue", description="Показать очередь треков") + async def queue(self, interaction: discord.Interaction): + player = get_player(interaction.guild.id) + embed = discord.Embed(title="📋 Очередь треков", color=0x5865F2) + + if player.current: + duration = fmt_duration(player.current.duration) if player.current.duration else "—" + embed.add_field( + name="▶️ Музончик играет", + value=f"**{player.current.title}** `{duration}`", + inline=False + ) + + if player.queue: + lines = [] + for i, url in enumerate(list(player.queue)[:10], 1): + lines.append(f"`{i}.` {url}") + if len(player.queue) > 10: + lines.append(f"... и ещё {len(player.queue) - 10} треков") + embed.add_field( + name=f"В очереди ({len(player.queue)})", + value="\n".join(lines), + inline=False + ) + else: + embed.add_field( + name="Очередь пуста", + value="Добавь треки через `/play`", + inline=False + ) + + await interaction.response.send_message(embed=embed) + + # ── /clear ──────────────────────────────── + @app_commands.command(name="clear", description="Очистить очередь треков") + async def clear(self, interaction: discord.Interaction): + player = get_player(interaction.guild.id) + count = len(player.queue) + player.queue.clear() + await interaction.response.send_message(f"🗑️ Лэ брат вьебал ({count} треков назхуй).") + + # ── /remove ─────────────────────────────── + @app_commands.command(name="remove", description="Вьебать трек из очереди по номеру") + @app_commands.describe(position="Номер трека в очереди") + async def remove(self, interaction: discord.Interaction, position: int): + player = get_player(interaction.guild.id) + if not player.queue: + return await interaction.response.send_message("Очередь пуста.") + if not 1 <= position <= len(player.queue): + return await interaction.response.send_message(f"❌ Укажи номер от 1 до {len(player.queue)}.") + + queue_list = list(player.queue) + removed = queue_list.pop(position - 1) + player.queue.clear() + player.queue.extend(queue_list) + await interaction.response.send_message(f"🗑️ Удалён трек `{position}`: {removed}") + +async def setup(bot: commands.Bot): + await bot.add_cog(Queue(bot)) \ No newline at end of file