Fixed queue, skip, adding tracks

This commit is contained in:
2026-03-10 00:15:05 +07:00
parent 6f29d7a5ff
commit 80ac57e9e8
5 changed files with 101 additions and 37 deletions

2
.idea/Bot.iml generated
View File

@@ -5,7 +5,7 @@
<sourceFolder url="file://$MODULE_DIR$/middleware" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.13 (bot1)" jdkType="Python SDK" />
<orderEntry type="jdk" jdkName="Python 3.12 (bot)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TemplatesService">

2
.idea/misc.xml generated
View File

@@ -3,5 +3,5 @@
<component name="Black">
<option name="sdkName" value="Python 3.13 (Bot)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 (bot1)" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (bot)" project-jdk-type="Python SDK" />
</project>

View File

@@ -12,7 +12,6 @@ RUN apt-get update \
nodejs \
npm \
build-essential \
curl \
&& rm -rf /var/lib/apt/lists/*
RUN pip install --upgrade pip
@@ -34,8 +33,10 @@ RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ffmpeg \
nodejs \
npm \
&& rm -rf /var/lib/apt/lists/*
RUN npm install -g deno
# Копируем python пакеты
COPY --from=builder /usr/local/lib/python3.13 /usr/local/lib/python3.13
COPY --from=builder /usr/local/bin /usr/local/bin

View File

@@ -2,28 +2,27 @@ from __future__ import annotations
import asyncio
from typing import Dict, List, Any, Optional
from middleware import logger
import discord
from discord.ext import commands
import yt_dlp
COG_TYPE="Music"
# yt-dlp конфиг с поддержкой поиска и node
YTDL_OPTIONS: Dict[str, Any] = {
"format": "bestaudio/best",
"noplaylist": True,
"quiet": True,
"default_search": "ytsearch1",
"default_search": "ytsearch",
"source_address": "0.0.0.0",
"js_runtimes": {
"node": {"path": "/usr/bin/node"}
},
"remote_components": {
"ejs:github": "github"
},
"extractor_args": {
"youtube": {
"player_client": ["web_music"]
}
}
# "js_runtimes": {
# "deno": {'path': "/usr/local/bin/deno"}
# }
}
FFMPEG_OPTIONS: Dict[str, str] = {
@@ -37,10 +36,28 @@ ytdl = yt_dlp.YoutubeDL(YTDL_OPTIONS)
class Music(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.queue: Dict[int, List[str]] = {} # guild_id -> список запросов
self.queue: Dict[int, List[Dict[str, str]]] = {}
logger.info(text="Инициализация Music", log_type="cog")
@discord.app_commands.command(name="queue", description="Посмотреть очередь")
async def getQueue(self, interaction: discord.Interaction):
safeQueue = "\n".join(
track["title"]
for guild_queue in self.queue.values()
for track in guild_queue
) or "❌ Пустая очередь"
logger.info(text=f"Текущая очередь:\n{safeQueue}", log_type="cog")
await interaction.response.send_message(f"Текущая очередь:\n{safeQueue}")
async def connect_voice(self, interaction: discord.Interaction) -> Optional[discord.VoiceClient]:
if not interaction.user.voice or not interaction.user.voice.channel:
logger.warning(
text=f"Юзер не в голосовом канале.\nЮзер: {interaction.user.voice}. Канал: {interaction.user.voice.channel}",
log_type="cog"
)
await interaction.response.send_message("❌ Вы должны быть в голосовом канале.", ephemeral=True)
return None
@@ -49,60 +66,87 @@ class Music(commands.Cog):
try:
voice_client = await interaction.user.voice.channel.connect()
except Exception as e:
logger.error(text=f"Не удалось подключиться\nОшибка: {e}", log_type="cog")
await interaction.response.send_message(f"Не удалось подключиться: {e}", ephemeral=True)
return None
return voice_client
async def get_audio(self, query: str) -> Optional[Dict[str, Any]]:
async def get_audio(self, query: str, interaction: discord.Interaction) -> Optional[Dict[str, Any]]:
try:
logger.info(
text=f"Поиск по ключевому слову {query}...",
log_type="cog"
)
if query.startswith("http"):
data = ytdl.extract_info(query, download=False)
data = ytdl.extract_info(query, download=False);
logger.info(
text=f"Предоставлена ссылка {query}",
log_type="cog"
)
else:
data = ytdl.extract_info(f"ytsearch:{query}", download=False)
if "entries" in data:
title = data["entries"][0]['title']
data = data["entries"][0]
logger.info(
text=f"Найдено {title}",
log_type="cog"
)
else:
logger.warning(
text=f"Ничего не найдено",
log_type="cog"
)
return data
except Exception:
except Exception as e:
logger.error(
text=f"Ошибка при получении аудио {e}",
log_type="cog"
)
await interaction.followup.send(f":x: Ошибка при получении аудио")
return None
async def play_next(self, guild_id: int, channel: discord.TextChannel) -> None:
async def play_next(self, guild_id: int, channel: discord.TextChannel):
if guild_id not in self.queue or not self.queue[guild_id]:
await channel.send("📭 Очередь пуста.")
return
next_query = self.queue[guild_id].pop(0)
# создаем фиктивный interaction для play
class DummyInteraction:
guild = channel.guild
user = channel.guild.me
response = type('Resp', (), {"send_message": lambda self, msg, ephemeral=False: asyncio.create_task(channel.send(msg))})()
track = self.queue[guild_id].pop(0)
await self.play(DummyInteraction(), next_query)
voice_client = channel.guild.voice_client
if not voice_client:
return
await self.start_track(voice_client, guild_id, channel, track)
@discord.app_commands.command(name="play", description="Воспроизвести трек или добавить в очередь")
async def play(self, interaction: discord.Interaction, query: str):
voice_client = await self.connect_voice(interaction)
if voice_client is None:
return
guild_id = interaction.guild.id
if guild_id not in self.queue:
self.queue[guild_id] = []
await interaction.response.defer()
data = await self.get_audio(query, interaction)
if voice_client.is_playing():
self.queue[guild_id].append(query)
await interaction.response.send_message(" Трек добавлен в очередь.")
return
data = await self.get_audio(query)
if not data:
await interaction.response.send_message("Не удалось найти трек.")
await interaction.followup.send("Не удалось найти трек.")
return
stream_url = data["url"]
title = data.get("title", "Неизвестный трек")
if voice_client.is_playing():
self.queue[guild_id].append({"title" : title, "stream_url" : stream_url})
await interaction.followup.send(f" Трек {title} добавлен в очередь.")
return
try:
source = discord.FFmpegOpusAudio(
stream_url,
@@ -110,10 +154,12 @@ class Music(commands.Cog):
**FFMPEG_OPTIONS
)
except Exception:
await interaction.response.send_message("❌ Ошибка воспроизведения.")
await interaction.followup.send("❌ Ошибка воспроизведения.")
return
await interaction.followup.send(f"▶️ Сейчас играет: **{title}**")
def after_playing(error):
logger.info(text=f"Включаем следующий трек...", log_type="cog")
fut = asyncio.run_coroutine_threadsafe(self.play_next(guild_id, interaction.channel), self.bot.loop)
try:
fut.result()
@@ -121,10 +167,13 @@ class Music(commands.Cog):
pass
voice_client.play(source, after=after_playing)
await interaction.response.send_message(f"▶️ Сейчас играет: **{title}**")
@discord.app_commands.command(name="skip", description="Пропустить текущий трек")
async def skip(self, interaction: discord.Interaction):
logger.info(
text=f"Скип...",
log_type="cog"
)
voice_client = interaction.guild.voice_client
if not voice_client or not voice_client.is_playing():
await interaction.response.send_message("❌ Сейчас ничего не играет.", ephemeral=True)
@@ -149,6 +198,22 @@ class Music(commands.Cog):
await voice_client.disconnect()
await interaction.response.send_message("👋 Бот вышел из голосового канала.")
async def start_track(self, voice_client, guild_id, channel, track):
source = discord.FFmpegOpusAudio(
track["stream_url"],
executable="ffmpeg",
**FFMPEG_OPTIONS
)
await channel.send(f"▶️ Сейчас играет: **{track['title']}**")
def after_playing(error):
asyncio.run_coroutine_threadsafe(
self.play_next(guild_id, channel),
self.bot.loop
)
voice_client.play(source, after=after_playing)
async def setup(bot: commands.Bot):
await bot.add_cog(Music(bot))

View File

@@ -18,8 +18,6 @@ dependencies = [
"davey(>=0.1.4)"
]
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"