Add files via upload
This commit is contained in:
167
cogs/music.py
Normal file
167
cogs/music.py
Normal 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="Уровень громкости (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))
|
||||
87
cogs/player.py
Normal file
87
cogs/player.py
Normal 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
70
cogs/queue.py
Normal 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))
|
||||
Reference in New Issue
Block a user