mirror of
https://github.com/deadcxap/YandexMusicDiscordBot.git
synced 2026-01-10 00:21:45 +03:00
20
.dockerignore
Normal file
20
.dockerignore
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
.env
|
||||||
14
Dockerfile
Normal file
14
Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
FROM python:3.13-slim
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y ffmpeg && apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY MusicBot /app/MusicBot
|
||||||
|
|
||||||
|
ENV PYTHONPATH=/app
|
||||||
|
|
||||||
|
CMD ["python", "./MusicBot/main.py"]
|
||||||
@@ -176,7 +176,7 @@ class General(Cog):
|
|||||||
@account.command(description="Ввести токен Яндекс Музыки.")
|
@account.command(description="Ввести токен Яндекс Музыки.")
|
||||||
@discord.option("token", type=discord.SlashCommandOptionType.string, description="Токен.")
|
@discord.option("token", type=discord.SlashCommandOptionType.string, description="Токен.")
|
||||||
async def login(self, ctx: discord.ApplicationContext, token: str) -> None:
|
async def login(self, ctx: discord.ApplicationContext, token: str) -> None:
|
||||||
logging.info(f"[GENERAL] Login command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
|
logging.info(f"[GENERAL] Login command invoked by user {ctx.author.id} in guild {ctx.guild_id}")
|
||||||
try:
|
try:
|
||||||
client = await YMClient(token).init()
|
client = await YMClient(token).init()
|
||||||
except UnauthorizedError:
|
except UnauthorizedError:
|
||||||
@@ -196,7 +196,7 @@ class General(Cog):
|
|||||||
|
|
||||||
@account.command(description="Удалить токен из базы данных бота.")
|
@account.command(description="Удалить токен из базы данных бота.")
|
||||||
async def remove(self, ctx: discord.ApplicationContext) -> None:
|
async def remove(self, ctx: discord.ApplicationContext) -> None:
|
||||||
logging.info(f"[GENERAL] Remove command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
|
logging.info(f"[GENERAL] Remove command invoked by user {ctx.author.id} in guild {ctx.guild_id}")
|
||||||
if not await self.users_db.get_ym_token(ctx.user.id):
|
if not await self.users_db.get_ym_token(ctx.user.id):
|
||||||
logging.info(f"[GENERAL] No token found for user {ctx.author.id}")
|
logging.info(f"[GENERAL] No token found for user {ctx.author.id}")
|
||||||
await ctx.respond('❌ Токен не указан.', delete_after=15, ephemeral=True)
|
await ctx.respond('❌ Токен не указан.', delete_after=15, ephemeral=True)
|
||||||
@@ -208,7 +208,7 @@ class General(Cog):
|
|||||||
|
|
||||||
@account.command(description="Получить плейлист «Мне нравится»")
|
@account.command(description="Получить плейлист «Мне нравится»")
|
||||||
async def likes(self, ctx: discord.ApplicationContext) -> None:
|
async def likes(self, ctx: discord.ApplicationContext) -> None:
|
||||||
logging.info(f"[GENERAL] Likes command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
|
logging.info(f"[GENERAL] Likes command invoked by user {ctx.author.id} in guild {ctx.guild_id}")
|
||||||
|
|
||||||
token = await self.users_db.get_ym_token(ctx.user.id)
|
token = await self.users_db.get_ym_token(ctx.user.id)
|
||||||
if not token:
|
if not token:
|
||||||
@@ -223,7 +223,13 @@ class General(Cog):
|
|||||||
await ctx.respond('❌ Неверный токен. Укажите новый через /account login.', delete_after=15, ephemeral=True)
|
await ctx.respond('❌ Неверный токен. Укажите новый через /account login.', delete_after=15, ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
likes = await client.users_likes_tracks()
|
try:
|
||||||
|
likes = await client.users_likes_tracks()
|
||||||
|
except UnauthorizedError:
|
||||||
|
logging.warning(f"[GENERAL] Unknown token error for user {ctx.user.id}")
|
||||||
|
await ctx.respond("❌ Произошла неизвестная ошибка при попытке получения лайков. Пожалуйста, сообщите об этом разработчику.", delete_after=15, ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
if likes is None:
|
if likes is None:
|
||||||
logging.info(f"[GENERAL] Failed to fetch likes for user {ctx.user.id}")
|
logging.info(f"[GENERAL] Failed to fetch likes for user {ctx.user.id}")
|
||||||
await ctx.respond('❌ Что-то пошло не так. Повторите попытку позже.', delete_after=15, ephemeral=True)
|
await ctx.respond('❌ Что-то пошло не так. Повторите попытку позже.', delete_after=15, ephemeral=True)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
from typing import Literal, cast
|
from typing import Literal, cast
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
@@ -19,7 +20,12 @@ class Settings(Cog):
|
|||||||
|
|
||||||
@settings.command(name="show", description="Показать текущие настройки бота.")
|
@settings.command(name="show", description="Показать текущие настройки бота.")
|
||||||
async def show(self, ctx: discord.ApplicationContext) -> None:
|
async def show(self, ctx: discord.ApplicationContext) -> None:
|
||||||
guild = await self.db.get_guild(ctx.guild.id, projection={'allow_change_connect': 1, 'vote_switch_track': 1, 'vote_add': 1})
|
if not ctx.guild_id:
|
||||||
|
logging.warning("[SETTINGS] Show command invoked without guild_id")
|
||||||
|
await ctx.respond("❌ Эта команда может быть использована только на сервере.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
guild = await self.db.get_guild(ctx.guild_id, projection={'allow_change_connect': 1, 'vote_switch_track': 1, 'vote_add': 1})
|
||||||
|
|
||||||
vote = "✅ - Переключение" if guild['vote_switch_track'] else "❌ - Переключение"
|
vote = "✅ - Переключение" if guild['vote_switch_track'] else "❌ - Переключение"
|
||||||
vote += "\n✅ - Добавление в очередь" if guild['vote_add'] else "\n❌ - Добавление в очередь"
|
vote += "\n✅ - Добавление в очередь" if guild['vote_add'] else "\n❌ - Добавление в очередь"
|
||||||
@@ -49,20 +55,25 @@ class Settings(Cog):
|
|||||||
if not member.guild_permissions.manage_channels:
|
if not member.guild_permissions.manage_channels:
|
||||||
await ctx.respond("❌ У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True)
|
await ctx.respond("❌ У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not ctx.guild_id:
|
||||||
|
logging.warning("[SETTINGS] Toggle command invoked without guild_id")
|
||||||
|
await ctx.respond("❌ Эта команда может быть использована только на сервере.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
guild = await self.db.get_guild(ctx.guild.id, projection={
|
guild = await self.db.get_guild(ctx.guild_id, projection={
|
||||||
'vote_switch_track': 1, 'vote_add': 1, 'allow_change_connect': 1})
|
'vote_switch_track': 1, 'vote_add': 1, 'allow_change_connect': 1})
|
||||||
|
|
||||||
if vote_type == 'Переключение':
|
if vote_type == 'Переключение':
|
||||||
await self.db.update(ctx.guild.id, {'vote_switch_track': not guild['vote_switch_track']})
|
await self.db.update(ctx.guild_id, {'vote_switch_track': not guild['vote_switch_track']})
|
||||||
response_message = "Голосование за переключение трека " + ("❌ выключено." if guild['vote_switch_track'] else "✅ включено.")
|
response_message = "Голосование за переключение трека " + ("❌ выключено." if guild['vote_switch_track'] else "✅ включено.")
|
||||||
|
|
||||||
elif vote_type == 'Добавление в очередь':
|
elif vote_type == 'Добавление в очередь':
|
||||||
await self.db.update(ctx.guild.id, {'vote_add': not guild['vote_add']})
|
await self.db.update(ctx.guild_id, {'vote_add': not guild['vote_add']})
|
||||||
response_message = "Голосование за добавление в очередь " + ("❌ выключено." if guild['vote_add'] else "✅ включено.")
|
response_message = "Голосование за добавление в очередь " + ("❌ выключено." if guild['vote_add'] else "✅ включено.")
|
||||||
|
|
||||||
elif vote_type == 'Добавление/Отключение бота':
|
elif vote_type == 'Добавление/Отключение бота':
|
||||||
await self.db.update(ctx.guild.id, {'allow_change_connect': not guild['allow_change_connect']})
|
await self.db.update(ctx.guild_id, {'allow_change_connect': not guild['allow_change_connect']})
|
||||||
response_message = f"Добавление/Отключение бота от канала теперь {'✅ разрешено' if not guild['allow_change_connect'] else '❌ запрещено'} участникам без прав управления каналом."
|
response_message = f"Добавление/Отключение бота от канала теперь {'✅ разрешено' if not guild['allow_change_connect'] else '❌ запрещено'} участникам без прав управления каналом."
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -352,8 +352,8 @@ class VoiceExtension:
|
|||||||
Returns:
|
Returns:
|
||||||
bool: Check result.
|
bool: Check result.
|
||||||
"""
|
"""
|
||||||
if not ctx.user or not ctx.guild:
|
if not ctx.user or not ctx.guild_id:
|
||||||
logging.warning("[VC_EXT] User or guild not found in context inside 'voice_check'")
|
logging.warning("[VC_EXT] User or guild id not found in context inside 'voice_check'")
|
||||||
await ctx.respond("❌ Что-то пошло не так. Попробуйте еще раз.", delete_after=15, ephemeral=True)
|
await ctx.respond("❌ Что-то пошло не так. Попробуйте еще раз.", delete_after=15, ephemeral=True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -379,7 +379,7 @@ class VoiceExtension:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
if check_vibe_privilage:
|
if check_vibe_privilage:
|
||||||
guild = await self.db.get_guild(ctx.guild.id, projection={'current_viber_id': 1, 'vibing': 1})
|
guild = await self.db.get_guild(ctx.guild_id, projection={'current_viber_id': 1, 'vibing': 1})
|
||||||
if guild['vibing'] and ctx.user.id != guild['current_viber_id']:
|
if guild['vibing'] and ctx.user.id != guild['current_viber_id']:
|
||||||
logging.debug("[VIBE] Context user is not the current viber")
|
logging.debug("[VIBE] Context user is not the current viber")
|
||||||
await ctx.respond("❌ Вы не можете взаимодействовать с чужой волной!", delete_after=15, ephemeral=True)
|
await ctx.respond("❌ Вы не можете взаимодействовать с чужой волной!", delete_after=15, ephemeral=True)
|
||||||
|
|||||||
@@ -203,16 +203,21 @@ class Voice(Cog, VoiceExtension):
|
|||||||
|
|
||||||
@voice.command(name="menu", description="Создать или обновить меню проигрывателя.")
|
@voice.command(name="menu", description="Создать или обновить меню проигрывателя.")
|
||||||
async def menu(self, ctx: discord.ApplicationContext) -> None:
|
async def menu(self, ctx: discord.ApplicationContext) -> None:
|
||||||
logging.info(f"[VOICE] Menu command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
|
logging.info(f"[VOICE] Menu command invoked by user {ctx.author.id} in guild {ctx.guild_id}")
|
||||||
if await self.voice_check(ctx) and not await self.send_menu_message(ctx):
|
if await self.voice_check(ctx) and not await self.send_menu_message(ctx):
|
||||||
await ctx.respond("❌ Не удалось создать меню.", ephemeral=True)
|
await ctx.respond("❌ Не удалось создать меню.", ephemeral=True)
|
||||||
|
|
||||||
@voice.command(name="join", description="Подключиться к голосовому каналу, в котором вы сейчас находитесь.")
|
@voice.command(name="join", description="Подключиться к голосовому каналу, в котором вы сейчас находитесь.")
|
||||||
async def join(self, ctx: discord.ApplicationContext) -> None:
|
async def join(self, ctx: discord.ApplicationContext) -> None:
|
||||||
logging.info(f"[VOICE] Join command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
|
logging.info(f"[VOICE] Join command invoked by user {ctx.author.id} in guild {ctx.guild_id}")
|
||||||
|
|
||||||
|
if not ctx.guild_id:
|
||||||
|
logging.warning("[VOICE] Join command invoked without guild_id")
|
||||||
|
await ctx.respond("❌ Эта команда может быть использована только на сервере.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
member = cast(discord.Member, ctx.author)
|
member = cast(discord.Member, ctx.author)
|
||||||
guild = await self.db.get_guild(ctx.guild.id, projection={'allow_change_connect': 1})
|
guild = await self.db.get_guild(ctx.guild_id, projection={'allow_change_connect': 1})
|
||||||
|
|
||||||
await ctx.defer(ephemeral=True)
|
await ctx.defer(ephemeral=True)
|
||||||
if not member.guild_permissions.manage_channels and not guild['allow_change_connect']:
|
if not member.guild_permissions.manage_channels and not guild['allow_change_connect']:
|
||||||
@@ -234,13 +239,18 @@ class Voice(Cog, VoiceExtension):
|
|||||||
|
|
||||||
@voice.command(description="Заставить бота покинуть голосовой канал.")
|
@voice.command(description="Заставить бота покинуть голосовой канал.")
|
||||||
async def leave(self, ctx: discord.ApplicationContext) -> None:
|
async def leave(self, ctx: discord.ApplicationContext) -> None:
|
||||||
logging.info(f"[VOICE] Leave command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
|
logging.info(f"[VOICE] Leave command invoked by user {ctx.author.id} in guild {ctx.guild_id}")
|
||||||
|
|
||||||
|
if not ctx.guild_id:
|
||||||
|
logging.warning("[VOICE] Leave command invoked without guild_id")
|
||||||
|
await ctx.respond("❌ Эта команда может быть использована только на сервере.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
member = cast(discord.Member, ctx.author)
|
member = cast(discord.Member, ctx.author)
|
||||||
guild = await self.db.get_guild(ctx.guild.id, projection={'allow_change_connect': 1})
|
guild = await self.db.get_guild(ctx.guild_id, projection={'allow_change_connect': 1})
|
||||||
|
|
||||||
if not member.guild_permissions.manage_channels and not guild['allow_change_connect']:
|
if not member.guild_permissions.manage_channels and not guild['allow_change_connect']:
|
||||||
logging.info(f"[VOICE] User {ctx.author.id} does not have permissions to execute leave command in guild {ctx.guild.id}")
|
logging.info(f"[VOICE] User {ctx.author.id} does not have permissions to execute leave command in guild {ctx.guild_id}")
|
||||||
await ctx.respond("❌ У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True)
|
await ctx.respond("❌ У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -252,13 +262,18 @@ class Voice(Cog, VoiceExtension):
|
|||||||
|
|
||||||
await vc.disconnect(force=True)
|
await vc.disconnect(force=True)
|
||||||
await ctx.respond("✅ Отключение успешно!", delete_after=15, ephemeral=True)
|
await ctx.respond("✅ Отключение успешно!", delete_after=15, ephemeral=True)
|
||||||
logging.info(f"[VOICE] Successfully disconnected from voice channel in guild {ctx.guild.id}")
|
logging.info(f"[VOICE] Successfully disconnected from voice channel in guild {ctx.guild_id}")
|
||||||
else:
|
else:
|
||||||
await ctx.respond("❌ Бот не подключен к голосовому каналу.", delete_after=15, ephemeral=True)
|
await ctx.respond("❌ Бот не подключен к голосовому каналу.", delete_after=15, ephemeral=True)
|
||||||
|
|
||||||
@queue.command(description="Очистить очередь треков и историю прослушивания.")
|
@queue.command(description="Очистить очередь треков и историю прослушивания.")
|
||||||
async def clear(self, ctx: discord.ApplicationContext) -> None:
|
async def clear(self, ctx: discord.ApplicationContext) -> None:
|
||||||
logging.info(f"[VOICE] Clear queue command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
|
logging.info(f"[VOICE] Clear queue command invoked by user {ctx.author.id} in guild {ctx.guild_id}")
|
||||||
|
|
||||||
|
if not ctx.guild_id:
|
||||||
|
logging.warning("[VOICE] Clear command invoked without guild_id")
|
||||||
|
await ctx.respond("❌ Эта команда может быть использована только на сервере.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
if not await self.voice_check(ctx):
|
if not await self.voice_check(ctx):
|
||||||
return
|
return
|
||||||
@@ -267,7 +282,7 @@ class Voice(Cog, VoiceExtension):
|
|||||||
channel = cast(discord.VoiceChannel, ctx.channel)
|
channel = cast(discord.VoiceChannel, ctx.channel)
|
||||||
|
|
||||||
if len(channel.members) > 2 and not member.guild_permissions.manage_channels:
|
if len(channel.members) > 2 and not member.guild_permissions.manage_channels:
|
||||||
logging.info(f"Starting vote for clearing queue in guild {ctx.guild.id}")
|
logging.info(f"Starting vote for clearing queue in guild {ctx.guild_id}")
|
||||||
|
|
||||||
response_message = f"{member.mention} хочет очистить историю прослушивания и очередь треков.\n\n Выполнить действие?."
|
response_message = f"{member.mention} хочет очистить историю прослушивания и очередь треков.\n\n Выполнить действие?."
|
||||||
message = cast(discord.Interaction, await ctx.respond(response_message, delete_after=60))
|
message = cast(discord.Interaction, await ctx.respond(response_message, delete_after=60))
|
||||||
@@ -289,19 +304,24 @@ class Voice(Cog, VoiceExtension):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
await self.db.update(ctx.guild.id, {'previous_tracks': [], 'next_tracks': []})
|
await self.db.update(ctx.guild_id, {'previous_tracks': [], 'next_tracks': []})
|
||||||
await ctx.respond("✅ Очередь и история сброшены.", delete_after=15, ephemeral=True)
|
await ctx.respond("✅ Очередь и история сброшены.", delete_after=15, ephemeral=True)
|
||||||
logging.info(f"[VOICE] Queue and history cleared in guild {ctx.guild.id}")
|
logging.info(f"[VOICE] Queue and history cleared in guild {ctx.guild_id}")
|
||||||
|
|
||||||
@queue.command(description="Получить очередь треков.")
|
@queue.command(description="Получить очередь треков.")
|
||||||
async def get(self, ctx: discord.ApplicationContext) -> None:
|
async def get(self, ctx: discord.ApplicationContext) -> None:
|
||||||
logging.info(f"[VOICE] Get queue command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
|
logging.info(f"[VOICE] Get queue command invoked by user {ctx.author.id} in guild {ctx.guild_id}")
|
||||||
|
|
||||||
|
if not ctx.guild_id:
|
||||||
|
logging.warning("[VOICE] Get command invoked without guild_id")
|
||||||
|
await ctx.respond("❌ Эта команда может быть использована только на сервере.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
if not await self.voice_check(ctx):
|
if not await self.voice_check(ctx):
|
||||||
return
|
return
|
||||||
await self.users_db.update(ctx.user.id, {'queue_page': 0})
|
await self.users_db.update(ctx.user.id, {'queue_page': 0})
|
||||||
|
|
||||||
tracks = await self.db.get_tracks_list(ctx.guild.id, 'next')
|
tracks = await self.db.get_tracks_list(ctx.guild_id, 'next')
|
||||||
if len(tracks) == 0:
|
if len(tracks) == 0:
|
||||||
await ctx.respond("❌ Очередь пуста.", ephemeral=True)
|
await ctx.respond("❌ Очередь пуста.", ephemeral=True)
|
||||||
return
|
return
|
||||||
@@ -309,11 +329,16 @@ class Voice(Cog, VoiceExtension):
|
|||||||
embed = generate_queue_embed(0, tracks)
|
embed = generate_queue_embed(0, tracks)
|
||||||
await ctx.respond(embed=embed, view=await QueueView(ctx).init(), ephemeral=True)
|
await ctx.respond(embed=embed, view=await QueueView(ctx).init(), ephemeral=True)
|
||||||
|
|
||||||
logging.info(f"[VOICE] Queue embed sent to user {ctx.author.id} in guild {ctx.guild.id}")
|
logging.info(f"[VOICE] Queue embed sent to user {ctx.author.id} in guild {ctx.guild_id}")
|
||||||
|
|
||||||
@voice.command(description="Прервать проигрывание, удалить историю, очередь и текущий плеер.")
|
@voice.command(description="Прервать проигрывание, удалить историю, очередь и текущий плеер.")
|
||||||
async def stop(self, ctx: discord.ApplicationContext) -> None:
|
async def stop(self, ctx: discord.ApplicationContext) -> None:
|
||||||
logging.info(f"[VOICE] Stop command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
|
logging.info(f"[VOICE] Stop command invoked by user {ctx.author.id} in guild {ctx.guild_id}")
|
||||||
|
|
||||||
|
if not ctx.guild_id:
|
||||||
|
logging.warning("[VOICE] Stop command invoked without guild_id")
|
||||||
|
await ctx.respond("❌ Эта команда может быть использована только на сервере.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
if not await self.voice_check(ctx):
|
if not await self.voice_check(ctx):
|
||||||
return
|
return
|
||||||
@@ -322,7 +347,7 @@ class Voice(Cog, VoiceExtension):
|
|||||||
channel = cast(discord.VoiceChannel, ctx.channel)
|
channel = cast(discord.VoiceChannel, ctx.channel)
|
||||||
|
|
||||||
if len(channel.members) > 2 and not member.guild_permissions.manage_channels:
|
if len(channel.members) > 2 and not member.guild_permissions.manage_channels:
|
||||||
logging.info(f"Starting vote for stopping playback in guild {ctx.guild.id}")
|
logging.info(f"Starting vote for stopping playback in guild {ctx.guild_id}")
|
||||||
|
|
||||||
response_message = f"{member.mention} хочет полностью остановить проигрывание.\n\n Выполнить действие?."
|
response_message = f"{member.mention} хочет полностью остановить проигрывание.\n\n Выполнить действие?."
|
||||||
message = cast(discord.Interaction, await ctx.respond(response_message, delete_after=60))
|
message = cast(discord.Interaction, await ctx.respond(response_message, delete_after=60))
|
||||||
@@ -351,7 +376,7 @@ class Voice(Cog, VoiceExtension):
|
|||||||
else:
|
else:
|
||||||
await ctx.respond("❌ Произошла ошибка при остановке воспроизведения.", delete_after=15, ephemeral=True)
|
await ctx.respond("❌ Произошла ошибка при остановке воспроизведения.", delete_after=15, ephemeral=True)
|
||||||
|
|
||||||
@voice.command(name='vibe', description="Запустить Мою Волну.")
|
@voice.command(description="Запустить Мою Волну.")
|
||||||
@discord.option(
|
@discord.option(
|
||||||
"запрос",
|
"запрос",
|
||||||
parameter_name='name',
|
parameter_name='name',
|
||||||
@@ -360,15 +385,20 @@ class Voice(Cog, VoiceExtension):
|
|||||||
autocomplete=discord.utils.basic_autocomplete(get_vibe_stations_suggestions),
|
autocomplete=discord.utils.basic_autocomplete(get_vibe_stations_suggestions),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
async def user_vibe(self, ctx: discord.ApplicationContext, name: str | None = None) -> None:
|
async def vibe(self, ctx: discord.ApplicationContext, name: str | None = None) -> None:
|
||||||
logging.info(f"[VOICE] Vibe (user) command invoked by user {ctx.user.id} in guild {ctx.guild_id}")
|
logging.info(f"[VOICE] Vibe (user) command invoked by user {ctx.user.id} in guild {ctx.guild_id}")
|
||||||
if not await self.voice_check(ctx):
|
if not await self.voice_check(ctx):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not ctx.guild_id:
|
||||||
|
logging.warning("[VOICE] Vibe command invoked without guild_id")
|
||||||
|
await ctx.respond("❌ Эта команда может быть использована только на сервере.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
guild = await self.db.get_guild(ctx.guild.id, projection={'current_menu': 1, 'vibing': 1})
|
guild = await self.db.get_guild(ctx.guild_id, projection={'current_menu': 1, 'vibing': 1})
|
||||||
|
|
||||||
if guild['vibing']:
|
if guild['vibing']:
|
||||||
logging.info(f"[VOICE] Action declined: vibing is already enabled in guild {ctx.guild.id}")
|
logging.info(f"[VOICE] Action declined: vibing is already enabled in guild {ctx.guild_id}")
|
||||||
await ctx.respond("❌ Моя Волна уже включена. Используйте /voice stop, чтобы остановить воспроизведение.", delete_after=15, ephemeral=True)
|
await ctx.respond("❌ Моя Волна уже включена. Используйте /voice stop, чтобы остановить воспроизведение.", delete_after=15, ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -411,7 +441,7 @@ class Voice(Cog, VoiceExtension):
|
|||||||
channel = cast(discord.VoiceChannel, ctx.channel)
|
channel = cast(discord.VoiceChannel, ctx.channel)
|
||||||
|
|
||||||
if len(channel.members) > 2 and not member.guild_permissions.manage_channels:
|
if len(channel.members) > 2 and not member.guild_permissions.manage_channels:
|
||||||
logging.info(f"Starting vote for starting vibe in guild {ctx.guild.id}")
|
logging.info(f"Starting vote for starting vibe in guild {ctx.guild_id}")
|
||||||
|
|
||||||
if _type == 'user' and _id == 'onyourwave':
|
if _type == 'user' and _id == 'onyourwave':
|
||||||
station = "Моя Волна"
|
station = "Моя Волна"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import os
|
||||||
from typing import Iterable, Any, cast
|
from typing import Iterable, Any, cast
|
||||||
from pymongo import AsyncMongoClient, ReturnDocument, UpdateOne
|
from pymongo import AsyncMongoClient, ReturnDocument, UpdateOne
|
||||||
from pymongo.asynchronous.collection import AsyncCollection
|
from pymongo.asynchronous.collection import AsyncCollection
|
||||||
@@ -6,7 +7,8 @@ from pymongo.results import UpdateResult
|
|||||||
from .user import User, ExplicitUser
|
from .user import User, ExplicitUser
|
||||||
from .guild import Guild, ExplicitGuild, MessageVotes
|
from .guild import Guild, ExplicitGuild, MessageVotes
|
||||||
|
|
||||||
client: AsyncMongoClient = AsyncMongoClient("mongodb://localhost:27017/")
|
mongo_server = os.getenv('MONGO_URI')
|
||||||
|
client: AsyncMongoClient = AsyncMongoClient(mongo_server)
|
||||||
|
|
||||||
db = client.YandexMusicBot
|
db = client.YandexMusicBot
|
||||||
users: AsyncCollection[ExplicitUser] = db.users
|
users: AsyncCollection[ExplicitUser] = db.users
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class PlayButton(Button, VoiceExtension):
|
|||||||
async def callback(self, interaction: Interaction) -> None:
|
async def callback(self, interaction: Interaction) -> None:
|
||||||
logging.debug(f"[FIND] Callback triggered for type: '{type(self.item).__name__}'")
|
logging.debug(f"[FIND] Callback triggered for type: '{type(self.item).__name__}'")
|
||||||
|
|
||||||
if not interaction.guild:
|
if not interaction.guild_id:
|
||||||
logging.info("[FIND] No guild found in PlayButton callback")
|
logging.info("[FIND] No guild found in PlayButton callback")
|
||||||
await interaction.respond("❌ Эта команда доступна только на серверах.", ephemeral=True, delete_after=15)
|
await interaction.respond("❌ Эта команда доступна только на серверах.", ephemeral=True, delete_after=15)
|
||||||
return
|
return
|
||||||
@@ -26,7 +26,7 @@ class PlayButton(Button, VoiceExtension):
|
|||||||
if not await self.voice_check(interaction):
|
if not await self.voice_check(interaction):
|
||||||
return
|
return
|
||||||
|
|
||||||
guild = await self.db.get_guild(interaction.guild.id, projection={'current_track': 1, 'current_menu': 1, 'vote_add': 1, 'vibing': 1})
|
guild = await self.db.get_guild(interaction.guild_id, projection={'current_track': 1, 'current_menu': 1, 'vote_add': 1, 'vibing': 1})
|
||||||
if guild['vibing']:
|
if guild['vibing']:
|
||||||
await interaction.respond("❌ Нельзя добавлять треки в очередь, пока запущена волна.", ephemeral=True, delete_after=15)
|
await interaction.respond("❌ Нельзя добавлять треки в очередь, пока запущена волна.", ephemeral=True, delete_after=15)
|
||||||
return
|
return
|
||||||
@@ -100,7 +100,7 @@ class PlayButton(Button, VoiceExtension):
|
|||||||
await response.add_reaction('❌')
|
await response.add_reaction('❌')
|
||||||
|
|
||||||
await self.db.update_vote(
|
await self.db.update_vote(
|
||||||
interaction.guild.id,
|
interaction.guild_id,
|
||||||
response.id,
|
response.id,
|
||||||
{
|
{
|
||||||
'positive_votes': list(),
|
'positive_votes': list(),
|
||||||
@@ -119,11 +119,11 @@ class PlayButton(Button, VoiceExtension):
|
|||||||
|
|
||||||
if guild['current_track']:
|
if guild['current_track']:
|
||||||
logging.debug(f"[FIND] Adding tracks to queue")
|
logging.debug(f"[FIND] Adding tracks to queue")
|
||||||
await self.db.modify_track(interaction.guild.id, tracks, 'next', 'extend')
|
await self.db.modify_track(interaction.guild_id, tracks, 'next', 'extend')
|
||||||
else:
|
else:
|
||||||
logging.debug(f"[FIND] Playing track")
|
logging.debug(f"[FIND] Playing track")
|
||||||
track = tracks.pop(0)
|
track = tracks.pop(0)
|
||||||
await self.db.modify_track(interaction.guild.id, tracks, 'next', 'extend')
|
await self.db.modify_track(interaction.guild_id, tracks, 'next', 'extend')
|
||||||
if not await self.play_track(interaction, track):
|
if not await self.play_track(interaction, track):
|
||||||
await interaction.respond('❌ Не удалось воспроизвести трек.', ephemeral=True, delete_after=15)
|
await interaction.respond('❌ Не удалось воспроизвести трек.', ephemeral=True, delete_after=15)
|
||||||
|
|
||||||
|
|||||||
@@ -593,7 +593,7 @@ class MenuView(View, VoiceExtension):
|
|||||||
if not self.ctx.guild_id:
|
if not self.ctx.guild_id:
|
||||||
return self
|
return self
|
||||||
|
|
||||||
self.guild = await self.db.get_guild(self.ctx.guild_id, projection={'repeat': 1, 'shuffle': 1, 'current_track': 1, 'vibing': 1})
|
self.guild = await self.db.get_guild(self.ctx.guild_id, projection={'repeat': 1, 'shuffle': 1, 'current_track': 1, 'current_menu': 1, 'vibing': 1})
|
||||||
|
|
||||||
if self.guild['repeat']:
|
if self.guild['repeat']:
|
||||||
self.repeat_button.style = ButtonStyle.success
|
self.repeat_button.style = ButtonStyle.success
|
||||||
|
|||||||
66
README.md
66
README.md
@@ -77,6 +77,72 @@ DEBUG='False' # Включение DEBUG логов (True/False)
|
|||||||
|
|
||||||
Запустите бота (`python ./MusicBot/main.py`).
|
Запустите бота (`python ./MusicBot/main.py`).
|
||||||
|
|
||||||
|
## Запуск в Docker
|
||||||
|
|
||||||
|
Возможен запуск как из командной строки, так и с помощью docker-compose.
|
||||||
|
|
||||||
|
### docker cli
|
||||||
|
|
||||||
|
>[!NOTE]
|
||||||
|
>При этом методе запуска вам необходимо самостоятельно установить MongoDB и указать адресс сервера в команде запуска.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name yandex-music-discord-bot \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-e TOKEN=XXXXXX \
|
||||||
|
-e EXPLICIT_EID=1325879701117472869 \
|
||||||
|
-e DEBUG=False \
|
||||||
|
-e MONGO_URI="mongodb://mongodb:27017" \
|
||||||
|
deadcxap/yandexmusicdiscordbot:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### docker-compose (рекомендованный)
|
||||||
|
|
||||||
|
>[!NOTE]
|
||||||
|
>При первом запуске БД и коллекции будут созданы автоматически.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
container_name: yandex-music-discord-bot
|
||||||
|
image: deadcxap/yandexmusicdiscordbot:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- mongodb
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
MONGO_URI: "mongodb://ymdb-mongodb:27017"
|
||||||
|
networks:
|
||||||
|
- ymdb_network
|
||||||
|
mongodb:
|
||||||
|
container_name: ymdb-mongodb
|
||||||
|
image: mongo:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- mongodb_data:/data/db
|
||||||
|
- ./init-mongodb.js:/docker-entrypoint-initdb.d/init-mongodb.js:ro
|
||||||
|
networks:
|
||||||
|
- ymdb_network
|
||||||
|
healthcheck:
|
||||||
|
test: echo 'db.runCommand("ping").ok' | mongo localhost:27017 --quiet
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongodb_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
ymdb_network:
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
## Настройка бота
|
## Настройка бота
|
||||||
|
|
||||||
Так должны выглядить настройки бота:
|
Так должны выглядить настройки бота:
|
||||||
|
|||||||
33
docker-compose.yml
Normal file
33
docker-compose.yml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
container_name: yandex-music-discord-bot
|
||||||
|
image: deadcxap/yandexmusicdiscordbot:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- mongodb
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
MONGO_URI: "mongodb://ymdb-mongodb:27017"
|
||||||
|
networks:
|
||||||
|
- ymdb_network
|
||||||
|
mongodb:
|
||||||
|
container_name: ymdb-mongodb
|
||||||
|
image: mongo:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- mongodb_data:/data/db
|
||||||
|
- ./init-mongodb.js:/docker-entrypoint-initdb.d/init-mongodb.js:ro
|
||||||
|
networks:
|
||||||
|
- ymdb_network
|
||||||
|
healthcheck:
|
||||||
|
test: echo 'db.runCommand("ping").ok' | mongo localhost:27017 --quiet
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongodb_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
ymdb_network:
|
||||||
3
init-mongodb.js
Normal file
3
init-mongodb.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
db = db.getSiblingDB('YandexMusicBot');
|
||||||
|
db.createCollection('guilds');
|
||||||
|
db.createCollection('users');
|
||||||
Reference in New Issue
Block a user