This commit is contained in:
@@ -78,7 +78,7 @@ temp/
|
|||||||
# Конфиденциальные файлы и настройки
|
# Конфиденциальные файлы и настройки
|
||||||
# -------------------------------
|
# -------------------------------
|
||||||
.env
|
.env
|
||||||
env/
|
/env
|
||||||
*.session
|
*.session
|
||||||
*.key
|
*.key
|
||||||
*.pem
|
*.pem
|
||||||
|
|||||||
54
Dockerfile
54
Dockerfile
@@ -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"]
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
154
bot/cogs/music.py
Normal 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))
|
||||||
1
main.py
1
main.py
@@ -15,3 +15,4 @@ async def main() -> None:
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
run(main())
|
run(main())
|
||||||
|
|
||||||
|
|||||||
@@ -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)"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user