Merge pull request 'Fixed queue, skip, adding tracks' (#4) from murprite/bot:murprite/fix-bot into master
Some checks failed
CI / basic-checks (push) Failing after 12s
Some checks failed
CI / basic-checks (push) Failing after 12s
Reviewed-on: #4
This commit was merged in pull request #4.
This commit is contained in:
2
.idea/Bot.iml
generated
2
.idea/Bot.iml
generated
@@ -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
2
.idea/misc.xml
generated
@@ -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>
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
@@ -16,9 +16,7 @@ dependencies = [
|
|||||||
"pynacl(>=1.6.2)",
|
"pynacl(>=1.6.2)",
|
||||||
"discord(>=2.3.2,<3.0.0)",
|
"discord(>=2.3.2,<3.0.0)",
|
||||||
"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"]
|
||||||
|
|||||||
Reference in New Issue
Block a user