Добавлено прослушивание музыки
Some checks failed
CI / basic-checks (push) Failing after 10s

This commit is contained in:
2026-03-07 13:11:18 +07:00
parent 2c6629f7f9
commit b89b30b7c5
7 changed files with 205 additions and 18 deletions

View File

@@ -78,7 +78,7 @@ temp/
# Конфиденциальные файлы и настройки # Конфиденциальные файлы и настройки
# ------------------------------- # -------------------------------
.env .env
env/ /env
*.session *.session
*.key *.key
*.pem *.pem

View File

@@ -1,26 +1,52 @@
# Используем официальный образ Python с подходящей версией # ---------- BUILDER ----------
FROM python:3.13-slim FROM python:3.13-slim AS builder
ENV PYTHONUNBUFFERED=1
ENV POETRY_VIRTUALENVS_CREATE=false
WORKDIR /build
# Устанавливаем системные зависимости только в builder
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ffmpeg \
nodejs \
npm \
build-essential \
curl \
&& rm -rf /var/lib/apt/lists/*
# Обновляем pip
RUN pip install --upgrade pip
# Устанавливаем Poetry # Устанавливаем Poetry
RUN pip install poetry RUN pip install poetry
# Устанавливаем рабочую директорию внутри контейнера # Копируем файлы зависимостей
WORKDIR /app
# Копируем файлы Poetry
COPY pyproject.toml poetry.lock* ./ COPY pyproject.toml poetry.lock* ./
# Настраиваем Poetry (не создавать виртуальное окружение внутри контейнера) # Устанавливаем зависимости
RUN poetry config virtualenvs.create false
# Устанавливаем зависимости через Poetry
RUN poetry install --no-interaction --no-ansi --no-root RUN poetry install --no-interaction --no-ansi --no-root
# Копируем все файлы проекта внутрь контейнера # ---------- RUNTIME ----------
COPY . . FROM python:3.13-slim
# Устанавливаем переменную окружения для буферизации
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
# Команда запуска — запуск скрипта main.py WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
# Копируем Python зависимости
COPY --from=builder /usr/local/lib/python3.13 /usr/local/lib/python3.13
COPY --from=builder /usr/local/bin /usr/local/bin
# Копируем ffmpeg бинарник
COPY --from=builder /usr/bin/ffmpeg /usr/bin/ffmpeg
# Копируем node runtime (для yt-dlp)
COPY --from=builder /usr/bin/node /usr/bin/node
COPY --from=builder /usr/bin/npm /usr/bin/npm
# Копируем проект
COPY . .
# Команда запуска
CMD ["python", "main.py"] CMD ["python", "main.py"]

View File

@@ -30,6 +30,7 @@ class Bot(commands.Bot):
intents.message_content = True intents.message_content = True
intents.members = True intents.members = True
intents.presences = True intents.presences = True
intents.voice_states = True
command_prefix: str = prefix or getattr(settings, "PREFIX", "!") command_prefix: str = prefix or getattr(settings, "PREFIX", "!")
@@ -74,6 +75,7 @@ class Bot(commands.Bot):
"bot.cogs.blacklist", "bot.cogs.blacklist",
"bot.cogs.reminders", "bot.cogs.reminders",
"bot.cogs.slash", "bot.cogs.slash",
"bot.cogs.music",
] ]
for cog in cogs: for cog in cogs:
try: try:

View File

@@ -2,3 +2,4 @@ from .events import *
from .blacklist import * from .blacklist import *
from .reminders import * from .reminders import *
from .moderation import * from .moderation import *
from .music import *

154
bot/cogs/music.py Normal file
View File

@@ -0,0 +1,154 @@
from __future__ import annotations
import asyncio
from typing import Optional, Dict, Any, List
import discord
from discord.ext import commands
import yt_dlp
YTDL_FORMAT_OPTIONS: Dict[str, Any] = {
"format": "bestaudio/best",
"noplaylist": True,
"quiet": True,
}
FFMPEG_OPTIONS: Dict[str, str] = {
"options": "-vn",
"before_options": "-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5",
}
ytdl: yt_dlp.YoutubeDL = yt_dlp.YoutubeDL(YTDL_FORMAT_OPTIONS)
class Music(commands.Cog):
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
self.queue: Dict[int, List[str]] = {}
async def connect_to_voice(self, ctx: commands.Context) -> Optional[discord.VoiceClient]:
if ctx.author.voice is None:
await ctx.send("❌ Вы должны находиться в голосовом канале.")
return None
voice_client: Optional[discord.VoiceClient] = ctx.voice_client
if voice_client is None:
try:
voice_client = await ctx.author.voice.channel.connect()
except Exception:
await ctx.send("Не удалось подключиться к голосовому каналу.")
return None
return voice_client
async def get_audio_data(self, query: str) -> Optional[Dict[str, Any]]:
"""Получает данные аудио по URL или поисковому запросу."""
try:
if query.startswith("http"):
data = ytdl.extract_info(query, download=False)
else:
data = ytdl.extract_info(f"ytsearch:{query}", download=False)
if "entries" in data:
data = data["entries"][0]
return data
except Exception:
return None
async def play_next(self, ctx: commands.Context) -> None:
guild_id = ctx.guild.id
voice_client = ctx.voice_client
if guild_id in self.queue and self.queue[guild_id]:
query = self.queue[guild_id].pop(0)
await self.play(ctx, query=query)
else:
await ctx.send("📭 Очередь пуста.")
@commands.command(name="play")
async def play(self, ctx: commands.Context, *, query: str) -> None:
voice_client = await self.connect_to_voice(ctx)
if voice_client is None:
return
guild_id = ctx.guild.id
if guild_id not in self.queue:
self.queue[guild_id] = []
if voice_client.is_playing():
self.queue[guild_id].append(query)
await ctx.send(" Трек добавлен в очередь.")
return
data = await self.get_audio_data(query)
if data is None:
await ctx.send("Не удалось найти трек.")
return
stream_url: str = data["url"]
title: str = data.get("title", "Неизвестный трек")
try:
source = await discord.FFmpegOpusAudio.from_probe(
stream_url,
executable="ffmpeg",
**FFMPEG_OPTIONS
)
except Exception:
await ctx.send("❌ Ошибка воспроизведения.")
return
def after_playing(error):
fut = asyncio.run_coroutine_threadsafe(self.play_next(ctx), self.bot.loop)
try:
fut.result()
except Exception:
pass
voice_client.play(source, after=after_playing)
await ctx.send(f"▶️ Сейчас играет: **{title}**")
@commands.command(name="skip")
async def skip(self, ctx: commands.Context) -> None:
voice_client: Optional[discord.VoiceClient] = ctx.voice_client
if voice_client is None or not voice_client.is_playing():
await ctx.send("❌ Сейчас ничего не играет.")
return
voice_client.stop()
await ctx.send("⏭️ Трек пропущен.")
@commands.command(name="stop")
async def stop(self, ctx: commands.Context) -> None:
voice_client: Optional[discord.VoiceClient] = ctx.voice_client
if voice_client is None:
await ctx.send("❌ Бот не подключён.")
return
guild_id = ctx.guild.id
self.queue[guild_id] = []
if voice_client.is_playing():
voice_client.stop()
await ctx.send("⏹️ Музыка остановлена.")
@commands.command(name="leave")
async def leave(self, ctx: commands.Context) -> None:
voice_client: Optional[discord.VoiceClient] = ctx.voice_client
if voice_client is None:
await ctx.send("❌ Бот не в голосовом канале.")
return
await voice_client.disconnect()
await ctx.send("👋 Бот вышел из голосового канала.")
async def setup(bot: commands.Bot) -> None:
await bot.add_cog(Music(bot))

View File

@@ -15,3 +15,4 @@ async def main() -> None:
if __name__ == "__main__": if __name__ == "__main__":
run(main()) run(main())

View File

@@ -8,12 +8,15 @@ authors = [
license = {text = "MIT"} license = {text = "MIT"}
requires-python = ">=3.11,<4.0" requires-python = ">=3.11,<4.0"
dependencies = [ dependencies = [
"discord (>=2.3.2,<3.0.0)",
"loguru (>=0.7.3,<0.8.0)", "loguru (>=0.7.3,<0.8.0)",
"email-validator (>=2.3.0,<3.0.0)", "email-validator (>=2.3.0,<3.0.0)",
"pydantic (>=2.12.5,<3.0.0)", "pydantic (>=2.12.5,<3.0.0)",
"pydantic-settings (>=2.12.0,<3.0.0)" "pydantic-settings (>=2.12.0,<3.0.0)",
] "yt_dlp(>=2026.3.3)",
"pynacl(>=1.6.2)",
"discord(>=2.3.2,<3.0.0)",
"davey(>=0.1.4)"
]