Add files via upload

This commit is contained in:
streloss
2026-03-11 22:51:17 +09:00
committed by GitHub
parent 5eb1c53ceb
commit 2dcaaf2290
3 changed files with 324 additions and 0 deletions

167
cogs/music.py Normal file
View File

@@ -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="Уровень громкости (1100)")
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))

87
cogs/player.py Normal file
View File

@@ -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

70
cogs/queue.py Normal file
View File

@@ -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))