Fixed queue, skip, adding tracks #4

Merged
NotFate merged 1 commits from murprite/bot:murprite/fix-bot into master 2026-03-10 14:52:14 +03:00
5 changed files with 101 additions and 37 deletions
Showing only changes of commit 80ac57e9e8 - Show all commits

2
.idea/Bot.iml generated
View File

@@ -5,7 +5,7 @@
<sourceFolder url="file://$MODULE_DIR$/middleware" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/middleware" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/.venv" /> <excludeFolder url="file://$MODULE_DIR$/.venv" />
</content> </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" /> <orderEntry type="sourceFolder" forTests="false" />
</component> </component>
<component name="TemplatesService"> <component name="TemplatesService">

2
.idea/misc.xml generated
View File

@@ -3,5 +3,5 @@
<component name="Black"> <component name="Black">
<option name="sdkName" value="Python 3.13 (Bot)" /> <option name="sdkName" value="Python 3.13 (Bot)" />
</component> </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> </project>

View File

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

View File

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

View File

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