mirror of
https://github.com/deadcxap/YandexMusicDiscordBot.git
synced 2026-01-10 00:21:45 +03:00
impr: Enforce menu view, vote improvement and bug fixes.
This commit is contained in:
@@ -41,13 +41,13 @@ async def get_search_suggestions(ctx: discord.AutocompleteContext) -> list[str]:
|
|||||||
|
|
||||||
logging.debug(f"[GENERAL] Searching for '{ctx.value}' for user {uid}")
|
logging.debug(f"[GENERAL] Searching for '{ctx.value}' for user {uid}")
|
||||||
|
|
||||||
if content_type == 'Трек' and search.tracks:
|
if content_type == 'Трек' and search.tracks is not None:
|
||||||
res = [f"{item.title} {f"({item.version})" if item.version else ''} - {", ".join(item.artists_name())}" for item in search.tracks.results]
|
res = [f"{item.title} {f"({item.version})" if item.version else ''} - {", ".join(item.artists_name())}" for item in search.tracks.results]
|
||||||
elif content_type == 'Альбом' and search.albums:
|
elif content_type == 'Альбом' and search.albums is not None:
|
||||||
res = [f"{item.title} - {", ".join(item.artists_name())}" for item in search.albums.results]
|
res = [f"{item.title} - {", ".join(item.artists_name())}" for item in search.albums.results]
|
||||||
elif content_type == 'Артист' and search.artists:
|
elif content_type == 'Артист' and search.artists is not None:
|
||||||
res = [f"{item.name}" for item in search.artists.results]
|
res = [f"{item.name}" for item in search.artists.results]
|
||||||
elif content_type == 'Плейлист' and search.playlists:
|
elif content_type == 'Плейлист' and search.playlists is not None:
|
||||||
res = [f"{item.title}" for item in search.playlists.results]
|
res = [f"{item.title}" for item in search.playlists.results]
|
||||||
else:
|
else:
|
||||||
logging.warning(f"[GENERAL] Invalid content type '{content_type}' for user {uid}")
|
logging.warning(f"[GENERAL] Invalid content type '{content_type}' for user {uid}")
|
||||||
@@ -108,7 +108,7 @@ class General(Cog):
|
|||||||
"Зарегистрируйте свой токен с помощью /login. Его можно получить [здесь](https://github.com/MarshalX/yandex-music-api/discussions/513).\n"
|
"Зарегистрируйте свой токен с помощью /login. Его можно получить [здесь](https://github.com/MarshalX/yandex-music-api/discussions/513).\n"
|
||||||
"Для получения помощи по конкретной команде, введите /help <команда>.\n"
|
"Для получения помощи по конкретной команде, введите /help <команда>.\n"
|
||||||
"Для изменения настроек необходимо иметь права управления каналами на сервере.\n\n"
|
"Для изменения настроек необходимо иметь права управления каналами на сервере.\n\n"
|
||||||
"**Для дополнительной помощи, присоединяйтесь к [серверу сообщества](https://discord.gg/gkmFDaPMeC).**"
|
"**Присоединяйтесь к нашему [серверу сообщества](https://discord.gg/TgnW8nfbFn)!**"
|
||||||
)
|
)
|
||||||
embed.add_field(
|
embed.add_field(
|
||||||
name='__Основные команды__',
|
name='__Основные команды__',
|
||||||
@@ -117,7 +117,6 @@ class General(Cog):
|
|||||||
`help`
|
`help`
|
||||||
`queue`
|
`queue`
|
||||||
`settings`
|
`settings`
|
||||||
`track`
|
|
||||||
`voice`"""
|
`voice`"""
|
||||||
)
|
)
|
||||||
embed.set_footer(text='©️ Bananchiki')
|
embed.set_footer(text='©️ Bananchiki')
|
||||||
@@ -149,30 +148,19 @@ class General(Cog):
|
|||||||
embed.description += (
|
embed.description += (
|
||||||
"`Примечание`: Только пользователи с разрешением управления каналом могут менять настройки.\n\n"
|
"`Примечание`: Только пользователи с разрешением управления каналом могут менять настройки.\n\n"
|
||||||
"Получить текущие настройки.\n```/settings show```\n"
|
"Получить текущие настройки.\n```/settings show```\n"
|
||||||
"Разрешить или запретить создание меню проигрывателя, когда в канале больше одного человека.\n```/settings menu```\n"
|
"Переключить параметр настроек.\n```/settings toggle <параметр>```\n"
|
||||||
"Разрешить или запретить голосование.\n```/settings vote <тип>```\n"
|
|
||||||
"Разрешить или запретить отключение/подключение бота к каналу участникам без прав управления каналом.\n```/settings connect```\n"
|
|
||||||
)
|
|
||||||
elif command == 'track':
|
|
||||||
embed.description += (
|
|
||||||
"`Примечание`: Если вы один в голосовом канале или имеете разрешение управления каналом, голосование не начинается.\n\n"
|
|
||||||
"Переключиться на следующий трек в очереди. \n```/track next```\n"
|
|
||||||
"Приостановить текущий трек.\n```/track pause```\n"
|
|
||||||
"Возобновить текущий трек.\n```/track resume```\n"
|
|
||||||
"Прервать проигрывание, удалить историю, очередь и текущий плеер.\n ```/track stop```\n"
|
|
||||||
"Добавить трек в плейлист «Мне нравится» или удалить его, если он уже там.\n```/track like```"
|
|
||||||
"Запустить Мою Волну по текущему треку.\n```/track vibe```"
|
|
||||||
)
|
)
|
||||||
elif command == 'voice':
|
elif command == 'voice':
|
||||||
embed.description += (
|
embed.description += (
|
||||||
"`Примечание`: Доступность меню и Моей Волны зависит от настроек сервера.\n\n"
|
"`Примечание`: Доступность меню и Моей Волны зависит от настроек сервера.\n\n"
|
||||||
"Присоединить бота в голосовой канал.\n```/voice join```\n"
|
"Присоединить бота в голосовой канал.\n```/voice join```\n"
|
||||||
"Заставить бота покинуть голосовой канал.\n ```/voice leave```\n"
|
"Заставить бота покинуть голосовой канал.\n ```/voice leave```\n"
|
||||||
|
"Прервать проигрывание, удалить историю, очередь и текущий плеер.\n ```/track stop```\n"
|
||||||
"Создать меню проигрывателя. \n```/voice menu```\n"
|
"Создать меню проигрывателя. \n```/voice menu```\n"
|
||||||
"Запустить станцию. Без уточнения станции, запускает Мою Волну.\n```/voice vibe <название станции>```"
|
"Запустить станцию. Без уточнения станции, запускает Мою Волну.\n```/voice vibe <название станции>```"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.respond('❌ Неизвестная команда.')
|
await ctx.respond('❌ Неизвестная команда.', delete_after=15, ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
await ctx.respond(embed=embed, ephemeral=True)
|
await ctx.respond(embed=embed, ephemeral=True)
|
||||||
@@ -236,6 +224,7 @@ class General(Cog):
|
|||||||
await ctx.respond('❌ У вас нет треков в плейлисте «Мне нравится».', delete_after=15, ephemeral=True)
|
await ctx.respond('❌ У вас нет треков в плейлисте «Мне нравится».', delete_after=15, ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
await ctx.defer() # Sometimes it takes a while to fetch all tracks, so we defer the response
|
||||||
real_tracks = await gather(*[track_short.fetch_track_async() for track_short in likes.tracks], return_exceptions=True)
|
real_tracks = await gather(*[track_short.fetch_track_async() for track_short in likes.tracks], return_exceptions=True)
|
||||||
tracks = [track for track in real_tracks if not isinstance(track, BaseException)] # Can't fetch user tracks
|
tracks = [track for track in real_tracks if not isinstance(track, BaseException)] # Can't fetch user tracks
|
||||||
|
|
||||||
|
|||||||
@@ -19,100 +19,53 @@ 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={
|
guild = await self.db.get_guild(ctx.guild.id, projection={'allow_change_connect': 1, 'vote_switch_track': 1, 'vote_add': 1})
|
||||||
'always_allow_menu': 1, 'allow_connect': 1, 'allow_disconnect': 1,
|
|
||||||
'vote_next_track': 1, 'vote_add_track': 1, 'vote_add_album': 1, 'vote_add_artist': 1, 'vote_add_playlist': 1
|
vote = "✅ - Переключение" if guild['vote_switch_track'] else "❌ - Переключение"
|
||||||
})
|
vote += "\n✅ - Добавление в очередь" if guild['vote_add'] else "\n❌ - Добавление в очередь"
|
||||||
|
|
||||||
|
connect = "\n✅ - Разрешено всем" if guild['allow_change_connect'] else "\n❌ - Только для участникам с правами управления каналом"
|
||||||
|
|
||||||
embed = discord.Embed(title="Настройки бота", color=0xfed42b)
|
embed = discord.Embed(title="Настройки бота", color=0xfed42b)
|
||||||
|
|
||||||
menu = "✅ - Всегда доступно" if guild['always_allow_menu'] else "❌ - Если в канале 1 человек."
|
|
||||||
|
|
||||||
vote = "✅ - Переключение" if guild['vote_next_track'] else "❌ - Переключение"
|
|
||||||
vote += "\n✅ - Добавление треков" if guild['vote_add_track'] else "\n❌ - Добавление треков"
|
|
||||||
vote += "\n✅ - Добавление альбомов" if guild['vote_add_album'] else "\n❌ - Добавление альбомов"
|
|
||||||
vote += "\n✅ - Добавление артистов" if guild['vote_add_artist'] else "\n❌ - Добавление артистов"
|
|
||||||
vote += "\n✅ - Добавление плейлистов" if guild['vote_add_playlist'] else "\n❌ - Добавление плейлистов"
|
|
||||||
|
|
||||||
connect = "\n✅ - Разрешено всем" if guild['allow_connect'] else "\n❌ - Только для участникам с правами управления каналом"
|
|
||||||
|
|
||||||
embed.add_field(name="__Меню проигрывателя__", value=menu, inline=False)
|
|
||||||
embed.add_field(name="__Голосование__", value=vote, inline=False)
|
embed.add_field(name="__Голосование__", value=vote, inline=False)
|
||||||
embed.add_field(name="__Подключение/Отключение__", value=connect, inline=False)
|
embed.add_field(name="__Подключение/Отключение бота__", value=connect, inline=False)
|
||||||
|
|
||||||
await ctx.respond(embed=embed, ephemeral=True)
|
await ctx.respond(embed=embed, ephemeral=True)
|
||||||
|
|
||||||
@settings.command(name="connect", description="Разрешить/запретить отключение/подключение бота к каналу участникам без прав управления каналом.")
|
|
||||||
async def connect(self, ctx: discord.ApplicationContext) -> None:
|
|
||||||
member = cast(discord.Member, ctx.author)
|
|
||||||
if not member.guild_permissions.manage_channels:
|
|
||||||
await ctx.respond("❌ У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
guild = await self.db.get_guild(ctx.guild.id, projection={'allow_connect': 1})
|
@settings.command(name="toggle", description="Переключить параметр настроек.")
|
||||||
await self.db.update(ctx.guild.id, {'allow_connect': not guild['allow_connect']})
|
|
||||||
await ctx.respond(f"Отключение/подключение бота к каналу теперь {'✅ разрешено' if not guild['allow_connect'] else '❌ запрещено'} участникам без прав управления каналом.", delete_after=15, ephemeral=True)
|
|
||||||
|
|
||||||
@settings.command(name="menu", description="Разрешить или запретить использование меню проигрывателя, если в канале больше одного человека.")
|
|
||||||
async def menu(self, ctx: discord.ApplicationContext) -> None:
|
|
||||||
member = cast(discord.Member, ctx.author)
|
|
||||||
if not member.guild_permissions.manage_channels:
|
|
||||||
await ctx.respond("❌ У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
guild = await self.db.get_guild(ctx.guild.id, projection={'always_allow_menu': 1})
|
|
||||||
await self.db.update(ctx.guild.id, {'always_allow_menu': not guild['always_allow_menu']})
|
|
||||||
await ctx.respond(f"Меню проигрывателя теперь {'✅ доступно' if not guild['always_allow_menu'] else '❌ недоступно'} в каналах с несколькими людьми.", delete_after=15, ephemeral=True)
|
|
||||||
|
|
||||||
@settings.command(name="vote", description="Настроить голосование.")
|
|
||||||
@discord.option(
|
@discord.option(
|
||||||
"vote_type",
|
"параметр",
|
||||||
|
parameter_name="vote_type",
|
||||||
description="Тип голосования.",
|
description="Тип голосования.",
|
||||||
type=discord.SlashCommandOptionType.string,
|
type=discord.SlashCommandOptionType.string,
|
||||||
choices=['+Всё', '-Всё', 'Переключение', 'Трек', 'Альбом', 'Плейлист'],
|
choices=['Переключение', 'Добавление в очередь', 'Добавление/Отключение бота']
|
||||||
default='+Всё'
|
|
||||||
)
|
)
|
||||||
async def vote(self, ctx: discord.ApplicationContext, vote_type: Literal['+Всё', '-Всё', 'Переключение', 'Трек', 'Альбом', 'Плейлист']) -> None:
|
async def toggle(
|
||||||
|
self,
|
||||||
|
ctx: discord.ApplicationContext,
|
||||||
|
vote_type: Literal['Переключение', 'Добавление в очередь', 'Добавление/Отключение бота']
|
||||||
|
) -> None:
|
||||||
member = cast(discord.Member, ctx.author)
|
member = cast(discord.Member, ctx.author)
|
||||||
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
|
||||||
|
|
||||||
guild = await self.db.get_guild(ctx.guild.id, projection={'vote_next_track': 1, 'vote_add_track': 1, 'vote_add_album': 1, 'vote_add_artist': 1, 'vote_add_playlist': 1})
|
guild = await self.db.get_guild(ctx.guild.id, projection={
|
||||||
|
'vote_switch_track': 1, 'vote_add': 1, 'allow_change_connect': 1})
|
||||||
if vote_type == '-Всё':
|
|
||||||
await self.db.update(ctx.guild.id, {
|
if vote_type == 'Переключение':
|
||||||
'vote_next_track': False,
|
await self.db.update(ctx.guild.id, {'vote_switch_track': not guild['vote_switch_track']})
|
||||||
'vote_add_track': False,
|
response_message = "Голосование за переключение трека " + ("❌ выключено." if guild['vote_switch_track'] else "✅ включено.")
|
||||||
'vote_add_album': False,
|
|
||||||
'vote_add_artist': False,
|
elif vote_type == 'Добавление в очередь':
|
||||||
'vote_add_playlist': False
|
await self.db.update(ctx.guild.id, {'vote_add': not guild['vote_add']})
|
||||||
}
|
response_message = "Голосование за добавление в очередь " + ("❌ выключено." if guild['vote_add'] else "✅ включено.")
|
||||||
)
|
|
||||||
response_message = "Голосование ❌ выключено."
|
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, {
|
response_message = f"Добавление/Отключение бота от канала теперь {'✅ разрешено' if not guild['allow_change_connect'] else '❌ запрещено'} участникам без прав управления каналом."
|
||||||
'vote_next_track': True,
|
|
||||||
'vote_add_track': True,
|
else:
|
||||||
'vote_add_album': True,
|
response_message = "❌ Неизвестный тип голосования."
|
||||||
'vote_add_artist': True,
|
|
||||||
'vote_add_playlist': True
|
|
||||||
}
|
|
||||||
)
|
|
||||||
response_message = "Голосование ✅ включено."
|
|
||||||
elif vote_type == 'Переключение':
|
|
||||||
await self.db.update(ctx.guild.id, {'vote_next_track': not guild['vote_next_track']})
|
|
||||||
response_message = "Голосование за переключение трека " + ("❌ выключено." if guild['vote_next_track'] else "✅ включено.")
|
|
||||||
elif vote_type == 'Трек':
|
|
||||||
await self.db.update(ctx.guild.id, {'vote_add_track': not guild['vote_add_track']})
|
|
||||||
response_message = "Голосование за добавление трека " + ("❌ выключено." if guild['vote_add_track'] else "✅ включено.")
|
|
||||||
elif vote_type == 'Альбом':
|
|
||||||
await self.db.update(ctx.guild.id, {'vote_add_album': not guild['vote_add_album']})
|
|
||||||
response_message = "Голосование за добавление альбома " + ("❌ выключено." if guild['vote_add_album'] else "✅ включено.")
|
|
||||||
elif vote_type == 'Артист':
|
|
||||||
await self.db.update(ctx.guild.id, {'vote_add_artist': not guild['vote_add_artist']})
|
|
||||||
response_message = "Голосование за добавление артиста " + ("❌ выключено." if guild['vote_add_artist'] else "✅ включено.")
|
|
||||||
elif vote_type == 'Плейлист':
|
|
||||||
await self.db.update(ctx.guild.id, {'vote_add_playlist': not guild['vote_add_playlist']})
|
|
||||||
response_message = "Голосование за добавление плейлиста " + ("❌ выключено." if guild['vote_add_playlist'] else "✅ включено.")
|
|
||||||
|
|
||||||
await ctx.respond(response_message, delete_after=15, ephemeral=True)
|
await ctx.respond(response_message, delete_after=15, ephemeral=True)
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ from yandex_music import Track, TrackShort, ClientAsync as YMClient
|
|||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ui import View
|
from discord.ui import View
|
||||||
from discord import Interaction, ApplicationContext, RawReactionActionEvent
|
from discord import Interaction, ApplicationContext, RawReactionActionEvent, VoiceChannel
|
||||||
|
|
||||||
from MusicBot.cogs.utils import generate_item_embed
|
from MusicBot.cogs.utils import generate_item_embed
|
||||||
from MusicBot.database import VoiceGuildsDatabase, BaseUsersDatabase, ExplicitGuild, ExplicitUser
|
from MusicBot.database import VoiceGuildsDatabase, BaseUsersDatabase, ExplicitGuild, ExplicitUser, MessageVotes
|
||||||
|
|
||||||
menu_views: dict[int, View] = {} # Store menu views and delete them when needed to prevent memory leaks for after callbacks.
|
menu_views: dict[int, View] = {} # Store menu views and delete them when needed to prevent memory leaks for after callbacks.
|
||||||
|
|
||||||
@@ -23,13 +23,16 @@ class VoiceExtension:
|
|||||||
self.db = VoiceGuildsDatabase()
|
self.db = VoiceGuildsDatabase()
|
||||||
self.users_db = BaseUsersDatabase()
|
self.users_db = BaseUsersDatabase()
|
||||||
|
|
||||||
async def send_menu_message(self, ctx: ApplicationContext | Interaction, *, disable: bool = False) -> bool:
|
async def send_menu_message(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, *, disable: bool = False) -> bool:
|
||||||
"""Send menu message to the channel and delete old menu message if exists. Return True if sent.
|
"""Send menu message to the channel and delete old menu message if exists. Return True if sent.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ctx (ApplicationContext | Interaction): Context.
|
ctx (ApplicationContext | Interaction): Context.
|
||||||
disable (bool, optional): Disable menu message. Defaults to False.
|
disable (bool, optional): Disable menu message. Defaults to False.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If bot instance is not set and ctx is RawReactionActionEvent.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if sent, False if not.
|
bool: True if sent, False if not.
|
||||||
"""
|
"""
|
||||||
@@ -65,7 +68,23 @@ class VoiceExtension:
|
|||||||
await message.delete()
|
await message.delete()
|
||||||
|
|
||||||
await self._update_menu_views_dict(ctx, disable=disable)
|
await self._update_menu_views_dict(ctx, disable=disable)
|
||||||
interaction = await ctx.respond(view=menu_views[ctx.guild_id], embed=embed)
|
|
||||||
|
if isinstance(ctx, (ApplicationContext, Interaction)):
|
||||||
|
interaction = await ctx.respond(view=menu_views[ctx.guild_id], embed=embed)
|
||||||
|
else:
|
||||||
|
if not self.bot:
|
||||||
|
raise ValueError("Bot instance is not set.")
|
||||||
|
|
||||||
|
channel = cast(VoiceChannel, self.bot.get_channel(ctx.channel_id))
|
||||||
|
if not channel:
|
||||||
|
logging.warning(f"[VC_EXT] Channel {ctx.channel_id} not found in guild {ctx.guild_id}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
interaction = await channel.send(
|
||||||
|
view=menu_views[ctx.guild_id],
|
||||||
|
embed=embed # type: ignore # Wrong typehints.
|
||||||
|
)
|
||||||
|
|
||||||
response = await interaction.original_response() if isinstance(interaction, discord.Interaction) else interaction
|
response = await interaction.original_response() if isinstance(interaction, discord.Interaction) else interaction
|
||||||
await self.db.update(ctx.guild_id, {'current_menu': response.id})
|
await self.db.update(ctx.guild_id, {'current_menu': response.id})
|
||||||
|
|
||||||
@@ -105,18 +124,17 @@ class VoiceExtension:
|
|||||||
await self.db.update(ctx.guild_id, {'current_menu': None})
|
await self.db.update(ctx.guild_id, {'current_menu': None})
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if menu:
|
if not menu:
|
||||||
logging.debug(f"[VC_EXT] Menu message {menu_mid} successfully fetched")
|
|
||||||
else:
|
|
||||||
logging.debug(f"[VC_EXT] Menu message {menu_mid} not found in guild {ctx.guild_id}")
|
logging.debug(f"[VC_EXT] Menu message {menu_mid} not found in guild {ctx.guild_id}")
|
||||||
await self.db.update(ctx.guild_id, {'current_menu': None})
|
await self.db.update(ctx.guild_id, {'current_menu': None})
|
||||||
|
return None
|
||||||
|
|
||||||
|
logging.debug(f"[VC_EXT] Menu message {menu_mid} successfully fetched")
|
||||||
return menu
|
return menu
|
||||||
|
|
||||||
async def update_menu_full(
|
async def update_menu_full(
|
||||||
self,
|
self,
|
||||||
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
|
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
|
||||||
menu_mid: int | None = None,
|
|
||||||
*,
|
*,
|
||||||
menu_message: discord.Message | None = None,
|
menu_message: discord.Message | None = None,
|
||||||
button_callback: bool = False
|
button_callback: bool = False
|
||||||
@@ -132,7 +150,7 @@ class VoiceExtension:
|
|||||||
Returns:
|
Returns:
|
||||||
bool: True if updated, False if not.
|
bool: True if updated, False if not.
|
||||||
"""
|
"""
|
||||||
logging.debug(
|
logging.info(
|
||||||
f"[VC_EXT] Updating menu embed using " + (
|
f"[VC_EXT] Updating menu embed using " + (
|
||||||
"interaction context" if isinstance(ctx, Interaction) else
|
"interaction context" if isinstance(ctx, Interaction) else
|
||||||
"application context" if isinstance(ctx, ApplicationContext) else
|
"application context" if isinstance(ctx, ApplicationContext) else
|
||||||
@@ -147,14 +165,11 @@ class VoiceExtension:
|
|||||||
logging.warning("[VC_EXT] Guild ID or User ID not found in context inside 'update_menu_embed'")
|
logging.warning("[VC_EXT] Guild ID or User ID not found in context inside 'update_menu_embed'")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
guild = await self.db.get_guild(gid, projection={'vibing': 1, 'current_track': 1})
|
guild = await self.db.get_guild(gid, projection={'vibing': 1, 'current_menu': 1, 'current_track': 1})
|
||||||
|
if not guild['current_menu']:
|
||||||
if not menu_message:
|
return False
|
||||||
if not menu_mid:
|
|
||||||
logging.warning("[VC_EXT] No menu message or menu message id provided")
|
|
||||||
return False
|
|
||||||
menu_message = await self.get_menu_message(ctx, menu_mid)
|
|
||||||
|
|
||||||
|
menu_message = await self.get_menu_message(ctx, guild['current_menu']) if not menu_message else menu_message
|
||||||
if not menu_message:
|
if not menu_message:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -168,6 +183,16 @@ class VoiceExtension:
|
|||||||
))
|
))
|
||||||
embed = await generate_item_embed(track, guild['vibing'])
|
embed = await generate_item_embed(track, guild['vibing'])
|
||||||
|
|
||||||
|
vc = await self.get_voice_client(ctx)
|
||||||
|
if not vc:
|
||||||
|
logging.warning("[VC_EXT] Voice client not found")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if vc.is_paused():
|
||||||
|
embed.set_footer(text='Приостановлено')
|
||||||
|
else:
|
||||||
|
embed.remove_footer()
|
||||||
|
|
||||||
await self._update_menu_views_dict(ctx)
|
await self._update_menu_views_dict(ctx)
|
||||||
try:
|
try:
|
||||||
if isinstance(ctx, Interaction) and button_callback:
|
if isinstance(ctx, Interaction) and button_callback:
|
||||||
@@ -186,7 +211,6 @@ class VoiceExtension:
|
|||||||
async def update_menu_view(
|
async def update_menu_view(
|
||||||
self,
|
self,
|
||||||
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
|
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
|
||||||
guild: ExplicitGuild,
|
|
||||||
*,
|
*,
|
||||||
menu_message: discord.Message | None = None,
|
menu_message: discord.Message | None = None,
|
||||||
button_callback: bool = False,
|
button_callback: bool = False,
|
||||||
@@ -206,6 +230,11 @@ class VoiceExtension:
|
|||||||
"""
|
"""
|
||||||
logging.debug("[VC_EXT] Updating menu view")
|
logging.debug("[VC_EXT] Updating menu view")
|
||||||
|
|
||||||
|
if not ctx.guild_id:
|
||||||
|
logging.warning("[VC_EXT] Guild ID not found in context inside 'update_menu_view'")
|
||||||
|
return False
|
||||||
|
|
||||||
|
guild = await self.db.get_guild(ctx.guild_id, projection={'current_menu': 1})
|
||||||
if not guild['current_menu']:
|
if not guild['current_menu']:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -217,10 +246,10 @@ class VoiceExtension:
|
|||||||
try:
|
try:
|
||||||
if isinstance(ctx, Interaction) and button_callback:
|
if isinstance(ctx, Interaction) and button_callback:
|
||||||
# If interaction from menu buttons
|
# If interaction from menu buttons
|
||||||
await ctx.edit(view=menu_views[guild['_id']])
|
await ctx.edit(view=menu_views[ctx.guild_id])
|
||||||
else:
|
else:
|
||||||
# If interaction from other buttons or commands. They should have their own response.
|
# If interaction from other buttons or commands. They should have their own response.
|
||||||
await menu_message.edit(view=menu_views[guild['_id']])
|
await menu_message.edit(view=menu_views[ctx.guild_id])
|
||||||
except discord.NotFound:
|
except discord.NotFound:
|
||||||
logging.warning("[VC_EXT] Menu message not found")
|
logging.warning("[VC_EXT] Menu message not found")
|
||||||
return False
|
return False
|
||||||
@@ -340,7 +369,7 @@ class VoiceExtension:
|
|||||||
|
|
||||||
if not isinstance(ctx.channel, discord.VoiceChannel):
|
if not isinstance(ctx.channel, discord.VoiceChannel):
|
||||||
logging.debug("[VC_EXT] User is not in a voice channel")
|
logging.debug("[VC_EXT] User is not in a voice channel")
|
||||||
await ctx.respond("❌ Вы должны отправить команду в голосовом канале.", delete_after=15, ephemeral=True)
|
await ctx.respond("❌ Вы должны отправить команду в чате голосового канала.", delete_after=15, ephemeral=True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if ctx.user.id not in ctx.channel.voice_states:
|
if ctx.user.id not in ctx.channel.voice_states:
|
||||||
@@ -408,7 +437,7 @@ class VoiceExtension:
|
|||||||
retry: bool = False
|
retry: bool = False
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""Download ``track`` by its id and play it in the voice channel. Return track title on success.
|
"""Download ``track`` by its id and play it in the voice channel. Return track title on success.
|
||||||
Send feedback for vibe track playing if vibing. Should be called if voice requirements are met.
|
Send vibe feedback for playing track if vibing. Should be called when voice requirements are met.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ctx (ApplicationContext | Interaction): Context.
|
ctx (ApplicationContext | Interaction): Context.
|
||||||
@@ -457,25 +486,31 @@ class VoiceExtension:
|
|||||||
|
|
||||||
if menu_message or guild['current_menu']:
|
if menu_message or guild['current_menu']:
|
||||||
# Updating menu message before playing to prevent delay and avoid FFMPEG lags.
|
# Updating menu message before playing to prevent delay and avoid FFMPEG lags.
|
||||||
await self.update_menu_full(ctx, guild['current_menu'], menu_message=menu_message, button_callback=button_callback)
|
await self.update_menu_full(ctx, menu_message=menu_message, button_callback=button_callback)
|
||||||
|
|
||||||
if not guild['vibing']:
|
if not guild['vibing']:
|
||||||
# Giving FFMPEG enough time to process the audio file
|
# Giving FFMPEG enough time to process the audio file
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
loop = self._get_current_event_loop(ctx)
|
loop = self._get_current_event_loop(ctx)
|
||||||
vc.play(song, after=lambda exc: asyncio.run_coroutine_threadsafe(self.next_track(ctx, after=True), loop))
|
try:
|
||||||
|
vc.play(song, after=lambda exc: asyncio.run_coroutine_threadsafe(self.next_track(ctx, after=True), loop))
|
||||||
|
except discord.errors.ClientException as e:
|
||||||
|
logging.error(f"[VC_EXT] Error while playing track '{track.title}': {e}")
|
||||||
|
if not isinstance(ctx, RawReactionActionEvent):
|
||||||
|
await ctx.respond(f"❌ Не удалось проиграть трек. Попробуйте сбросить меню.", delete_after=15, ephemeral=True)
|
||||||
|
|
||||||
logging.info(f"[VC_EXT] Playing track '{track.title}'")
|
logging.info(f"[VC_EXT] Playing track '{track.title}'")
|
||||||
await self.db.update(gid, {'is_stopped': False})
|
await self.db.update(gid, {'is_stopped': False})
|
||||||
|
|
||||||
if guild['vibing']:
|
if guild['vibing']:
|
||||||
await self._my_vibe_send_start_feedback(ctx, track, uid)
|
await self._my_vibe_start_feedback(ctx, track, uid)
|
||||||
|
|
||||||
return track.title
|
return track.title
|
||||||
|
|
||||||
async def stop_playing(
|
async def stop_playing(
|
||||||
self, ctx: ApplicationContext | Interaction | RawReactionActionEvent,
|
self,
|
||||||
|
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
|
||||||
*,
|
*,
|
||||||
vc: discord.VoiceClient | None = None,
|
vc: discord.VoiceClient | None = None,
|
||||||
full: bool = False
|
full: bool = False
|
||||||
@@ -514,7 +549,7 @@ class VoiceExtension:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
if guild['vibing'] and guild['current_track']:
|
if guild['vibing'] and guild['current_track']:
|
||||||
if not await self._my_vibe_send_stop_feedback(ctx, guild, user):
|
if not await self._my_vibe_stop_feedback(ctx, guild, user):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@@ -571,10 +606,10 @@ class VoiceExtension:
|
|||||||
await self.db.modify_track(gid, guild['current_track'], 'previous', 'insert')
|
await self.db.modify_track(gid, guild['current_track'], 'previous', 'insert')
|
||||||
|
|
||||||
if after and guild['current_menu']:
|
if after and guild['current_menu']:
|
||||||
await self.update_menu_view(ctx, guild, menu_message=menu_message, disable=True)
|
await self.update_menu_view(ctx, menu_message=menu_message, disable=True)
|
||||||
|
|
||||||
if guild['vibing'] and guild['current_track'] and not isinstance(ctx, discord.RawReactionActionEvent):
|
if guild['vibing'] and guild['current_track'] and not isinstance(ctx, discord.RawReactionActionEvent):
|
||||||
if not await self._send_next_vibe_feedback(ctx, guild, user, client, after=after):
|
if not await self._my_vibe_feedback(ctx, guild, user, client, after=after):
|
||||||
await ctx.respond("❌ Что-то пошло не так. Попробуйте снова.", ephemeral=True)
|
await ctx.respond("❌ Что-то пошло не так. Попробуйте снова.", ephemeral=True)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -598,7 +633,7 @@ class VoiceExtension:
|
|||||||
next_track = await self.db.get_track(gid, 'next')
|
next_track = await self.db.get_track(gid, 'next')
|
||||||
|
|
||||||
if next_track:
|
if next_track:
|
||||||
title = await self._play_next_track(ctx, next_track, client=client, vc=vc, button_callback=button_callback)
|
title = await self._play_track(ctx, next_track, client=client, vc=vc, button_callback=button_callback)
|
||||||
|
|
||||||
if after and not guild['current_menu']:
|
if after and not guild['current_menu']:
|
||||||
if isinstance(ctx, discord.RawReactionActionEvent):
|
if isinstance(ctx, discord.RawReactionActionEvent):
|
||||||
@@ -618,7 +653,7 @@ class VoiceExtension:
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def prev_track(self, ctx: ApplicationContext | Interaction, button_callback: bool = False) -> str | None:
|
async def previous_track(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, button_callback: bool = False) -> str | None:
|
||||||
"""Switch to the previous track in the queue. Repeat current track if no previous one found.
|
"""Switch to the previous track in the queue. Repeat current track if no previous one found.
|
||||||
Return track title on success.
|
Return track title on success.
|
||||||
|
|
||||||
@@ -629,12 +664,17 @@ class VoiceExtension:
|
|||||||
Returns:
|
Returns:
|
||||||
(str | None): Track title or None.
|
(str | None): Track title or None.
|
||||||
"""
|
"""
|
||||||
if not ctx.guild or not ctx.user:
|
logging.debug("[VC_EXT] Switching to previous track")
|
||||||
logging.warning("Guild or User not found in context inside 'prev_track'")
|
|
||||||
|
gid = ctx.guild_id
|
||||||
|
uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None
|
||||||
|
|
||||||
|
if not gid or not uid:
|
||||||
|
logging.warning("[VC_EXT] Guild ID or User ID not found in context inside 'next_track'")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
current_track = await self.db.get_track(ctx.guild.id, 'current')
|
current_track = await self.db.get_track(gid, 'current')
|
||||||
prev_track = await self.db.get_track(ctx.guild.id, 'previous')
|
prev_track = await self.db.get_track(gid, 'previous')
|
||||||
|
|
||||||
if prev_track:
|
if prev_track:
|
||||||
logging.debug("[VC_EXT] Previous track found")
|
logging.debug("[VC_EXT] Previous track found")
|
||||||
@@ -647,7 +687,7 @@ class VoiceExtension:
|
|||||||
track = None
|
track = None
|
||||||
|
|
||||||
if track:
|
if track:
|
||||||
return await self._play_next_track(ctx, track, button_callback=button_callback)
|
return await self._play_track(ctx, track, button_callback=button_callback)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -721,7 +761,8 @@ class VoiceExtension:
|
|||||||
add_func = client.users_dislikes_tracks_add
|
add_func = client.users_dislikes_tracks_add
|
||||||
remove_func = client.users_dislikes_tracks_remove
|
remove_func = client.users_dislikes_tracks_remove
|
||||||
|
|
||||||
if not tracks:
|
if tracks is None:
|
||||||
|
logging.debug(f"[VC_EXT] No {action}s found")
|
||||||
return (False, None)
|
return (False, None)
|
||||||
|
|
||||||
if str(current_track['id']) not in [str(track.id) for track in tracks]:
|
if str(current_track['id']) not in [str(track.id) for track in tracks]:
|
||||||
@@ -771,6 +812,82 @@ class VoiceExtension:
|
|||||||
self._ym_clients[token] = client
|
self._ym_clients[token] = client
|
||||||
return client
|
return client
|
||||||
|
|
||||||
|
async def proccess_vote(self, ctx: RawReactionActionEvent, guild: ExplicitGuild, channel: VoiceChannel, vote_data: MessageVotes) -> bool:
|
||||||
|
"""Proccess vote and perform action from `vote_data` and respond. Return True on success.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ctx (RawReactionActionEvent): Context.
|
||||||
|
guild (ExplicitGuild): Guild data.
|
||||||
|
message (Message): Message.
|
||||||
|
vote_data (MessageVotes): Vote data.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: Success status.
|
||||||
|
"""
|
||||||
|
logging.info(f"[VOICE] Performing '{vote_data['action']}' action for message {ctx.message_id}")
|
||||||
|
|
||||||
|
if not guild['current_menu']:
|
||||||
|
await self.send_menu_message(ctx)
|
||||||
|
|
||||||
|
if vote_data['action'] in ('next', 'previous'):
|
||||||
|
if not guild.get(f'{vote_data['action']}_tracks'):
|
||||||
|
await channel.send(content=f"❌ Очередь пуста!", delete_after=15)
|
||||||
|
|
||||||
|
elif not (await self.next_track(ctx) if vote_data['action'] == 'next' else await self.previous_track(ctx)):
|
||||||
|
await channel.send(content=f"❌ Ошибка при смене трека! Попробуйте ещё раз.", delete_after=15)
|
||||||
|
|
||||||
|
elif vote_data['action'] == 'add_track':
|
||||||
|
track = vote_data['vote_content']
|
||||||
|
if not track:
|
||||||
|
logging.info(f"[VOICE] Recieved empty vote context for message {ctx.message_id}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
await self.db.modify_track(guild['_id'], track, 'next', 'append')
|
||||||
|
|
||||||
|
if guild['current_track']:
|
||||||
|
await channel.send(content=f"✅ Трек был добавлен в очередь!", delete_after=15)
|
||||||
|
else:
|
||||||
|
if not await self.next_track(ctx):
|
||||||
|
await channel.send(content=f"❌ Ошибка при воспроизведении! Попробуйте ещё раз.", delete_after=15)
|
||||||
|
|
||||||
|
elif vote_data['action'] in ('add_album', 'add_artist', 'add_playlist'):
|
||||||
|
tracks = vote_data['vote_content']
|
||||||
|
if not tracks:
|
||||||
|
logging.info(f"[VOICE] Recieved empty vote context for message {ctx.message_id}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
await self.db.update(guild['_id'], {'is_stopped': False})
|
||||||
|
await self.db.modify_track(guild['_id'], tracks, 'next', 'extend')
|
||||||
|
|
||||||
|
if guild['current_track']:
|
||||||
|
await channel.send(content=f"✅ Контент был добавлен в очередь!", delete_after=15)
|
||||||
|
else:
|
||||||
|
if not await self.next_track(ctx):
|
||||||
|
await channel.send(content=f"❌ Ошибка при воспроизведении! Попробуйте ещё раз.", delete_after=15)
|
||||||
|
|
||||||
|
elif vote_data['action'] == 'play/pause':
|
||||||
|
vc = await self.get_voice_client(ctx)
|
||||||
|
if not vc:
|
||||||
|
await channel.send(content=f"❌ Ошибка при изменении воспроизведения! Попробуйте ещё раз.", delete_after=15)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if vc.is_playing():
|
||||||
|
vc.pause()
|
||||||
|
else:
|
||||||
|
vc.resume()
|
||||||
|
|
||||||
|
await self.update_menu_full(ctx)
|
||||||
|
|
||||||
|
elif vote_data['action'] in ('repeat', 'shuffle'):
|
||||||
|
await self.db.update(guild['_id'], {vote_data['action']: not guild[vote_data['action']]})
|
||||||
|
await self.update_menu_view(ctx)
|
||||||
|
|
||||||
|
else:
|
||||||
|
logging.warning(f"[VOICE] Unknown action '{vote_data['action']}' for message {ctx.message_id}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
async def _update_menu_views_dict(
|
async def _update_menu_views_dict(
|
||||||
self,
|
self,
|
||||||
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
|
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
|
||||||
@@ -836,7 +953,7 @@ class VoiceExtension:
|
|||||||
})
|
})
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def _my_vibe_send_start_feedback(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, track: Track, uid: int):
|
async def _my_vibe_start_feedback(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, track: Track, uid: int):
|
||||||
"""Send vibe start feedback to Yandex Music. Return True on success.
|
"""Send vibe start feedback to Yandex Music. Return True on success.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -861,7 +978,7 @@ class VoiceExtension:
|
|||||||
logging.debug(f"[VIBE] Track started feedback: {feedback}")
|
logging.debug(f"[VIBE] Track started feedback: {feedback}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def _my_vibe_send_stop_feedback(
|
async def _my_vibe_stop_feedback(
|
||||||
self,
|
self,
|
||||||
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
|
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
|
||||||
guild: ExplicitGuild,
|
guild: ExplicitGuild,
|
||||||
@@ -904,7 +1021,7 @@ class VoiceExtension:
|
|||||||
logging.info(f"[VOICE] User {user['_id']} finished vibing with result: {res}")
|
logging.info(f"[VOICE] User {user['_id']} finished vibing with result: {res}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def _send_next_vibe_feedback(
|
async def _my_vibe_feedback(
|
||||||
self,
|
self,
|
||||||
ctx: ApplicationContext | Interaction,
|
ctx: ApplicationContext | Interaction,
|
||||||
guild: ExplicitGuild,
|
guild: ExplicitGuild,
|
||||||
@@ -926,6 +1043,7 @@ class VoiceExtension:
|
|||||||
Returns:
|
Returns:
|
||||||
bool: True on success, False otherwise.
|
bool: True on success, False otherwise.
|
||||||
"""
|
"""
|
||||||
|
# TODO: Should be refactored to prevent duplication with `_my_vibe_stop_feedback` and `_my_vibe_start_feedback`
|
||||||
logging.debug(f"[VC_EXT] Sending vibe feedback, after: {after}")
|
logging.debug(f"[VC_EXT] Sending vibe feedback, after: {after}")
|
||||||
|
|
||||||
if not user['vibe_type'] or not user['vibe_id']:
|
if not user['vibe_type'] or not user['vibe_id']:
|
||||||
@@ -964,21 +1082,21 @@ class VoiceExtension:
|
|||||||
|
|
||||||
return feedback
|
return feedback
|
||||||
|
|
||||||
async def _play_next_track(
|
async def _play_track(
|
||||||
self,
|
self,
|
||||||
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
|
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
|
||||||
next_track: dict[str, Any],
|
track: dict[str, Any],
|
||||||
*,
|
*,
|
||||||
client: YMClient | None = None,
|
client: YMClient | None = None,
|
||||||
vc: discord.VoiceClient | None = None,
|
vc: discord.VoiceClient | None = None,
|
||||||
menu_message: discord.Message | None = None,
|
menu_message: discord.Message | None = None,
|
||||||
button_callback: bool = False,
|
button_callback: bool = False,
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""Play the `next_track` in the voice channel. Avoids additional button and vibe checks.
|
"""Play `track` in the voice channel. Avoids additional vibe checks used in `next_track` and `previous_track`.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context.
|
ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context.
|
||||||
next_track (dict[str, Any]): Next track to play.
|
track (dict[str, Any]): Track to play.
|
||||||
vc (discord.VoiceClient | None, optional): Voice client. Defaults to None.
|
vc (discord.VoiceClient | None, optional): Voice client. Defaults to None.
|
||||||
menu_message (discord.Message | None, optional): Menu message to update. Defaults to None.
|
menu_message (discord.Message | None, optional): Menu message to update. Defaults to None.
|
||||||
button_callback (bool, optional): Should be True if the function is being called from button callback. Defaults to False.
|
button_callback (bool, optional): Should be True if the function is being called from button callback. Defaults to False.
|
||||||
@@ -986,6 +1104,7 @@ class VoiceExtension:
|
|||||||
Returns:
|
Returns:
|
||||||
str | None: Song title or None.
|
str | None: Song title or None.
|
||||||
"""
|
"""
|
||||||
|
# TODO: This should be refactored to avoid code duplication with `next_track` and `previous_track`.
|
||||||
client = await self.init_ym_client(ctx) if not client else client
|
client = await self.init_ym_client(ctx) if not client else client
|
||||||
|
|
||||||
if not client:
|
if not client:
|
||||||
@@ -998,7 +1117,7 @@ class VoiceExtension:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
ym_track = cast(Track, Track.de_json(
|
ym_track = cast(Track, Track.de_json(
|
||||||
next_track,
|
track,
|
||||||
client=client # type: ignore # Async client can be used here.
|
client=client # type: ignore # Async client can be used here.
|
||||||
))
|
))
|
||||||
return await self.play_track(
|
return await self.play_track(
|
||||||
@@ -1010,7 +1129,7 @@ class VoiceExtension:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _get_current_event_loop(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent) -> asyncio.AbstractEventLoop:
|
def _get_current_event_loop(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent) -> asyncio.AbstractEventLoop:
|
||||||
"""Get the current event loop. If the context is a RawReactionActionEvent, get the loop from the bot.
|
"""Get the current event loop. If the context is a RawReactionActionEvent, get the loop from the self.bot instance.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context.
|
ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context.
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ class Voice(Cog, VoiceExtension):
|
|||||||
|
|
||||||
voice = discord.SlashCommandGroup("voice", "Команды, связанные с голосовым каналом.")
|
voice = discord.SlashCommandGroup("voice", "Команды, связанные с голосовым каналом.")
|
||||||
queue = discord.SlashCommandGroup("queue", "Команды, связанные с очередью треков.")
|
queue = discord.SlashCommandGroup("queue", "Команды, связанные с очередью треков.")
|
||||||
track = discord.SlashCommandGroup("track", "Команды, связанные с треками в голосовом канале.")
|
|
||||||
|
|
||||||
def __init__(self, bot: discord.Bot):
|
def __init__(self, bot: discord.Bot):
|
||||||
VoiceExtension.__init__(self, bot)
|
VoiceExtension.__init__(self, bot)
|
||||||
@@ -48,7 +47,7 @@ class Voice(Cog, VoiceExtension):
|
|||||||
@Cog.listener()
|
@Cog.listener()
|
||||||
async def on_voice_state_update(self, member: discord.Member, before: discord.VoiceState, after: discord.VoiceState) -> None:
|
async def on_voice_state_update(self, member: discord.Member, before: discord.VoiceState, after: discord.VoiceState) -> None:
|
||||||
gid = member.guild.id
|
gid = member.guild.id
|
||||||
guild = await self.db.get_guild(gid, projection={'current_menu': 1, 'always_allow_menu': 1})
|
guild = await self.db.get_guild(gid, projection={'current_menu': 1})
|
||||||
|
|
||||||
channel = after.channel or before.channel
|
channel = after.channel or before.channel
|
||||||
if not channel:
|
if not channel:
|
||||||
@@ -87,18 +86,7 @@ class Voice(Cog, VoiceExtension):
|
|||||||
'repeat': False, 'shuffle': False, 'is_stopped': True
|
'repeat': False, 'shuffle': False, 'is_stopped': True
|
||||||
})
|
})
|
||||||
vc.stop()
|
vc.stop()
|
||||||
elif len(channel.members) > 2 and not guild['always_allow_menu']:
|
|
||||||
if guild['current_menu']:
|
|
||||||
logging.info(f"[VOICE] Disabling current menu for guild {gid} due to multiple members")
|
|
||||||
|
|
||||||
await self.db.update(gid, {'current_menu': None, 'repeat': False, 'shuffle': False, 'vibing': False})
|
|
||||||
try:
|
|
||||||
message = await channel.fetch_message(guild['current_menu'])
|
|
||||||
await message.delete()
|
|
||||||
await channel.send("Меню отключено из-за большого количества участников.", delete_after=15)
|
|
||||||
except (discord.NotFound, discord.Forbidden):
|
|
||||||
pass
|
|
||||||
|
|
||||||
if member.guild.id in menu_views:
|
if member.guild.id in menu_views:
|
||||||
menu_views[member.guild.id].stop()
|
menu_views[member.guild.id].stop()
|
||||||
del menu_views[member.guild.id]
|
del menu_views[member.guild.id]
|
||||||
@@ -138,7 +126,7 @@ class Voice(Cog, VoiceExtension):
|
|||||||
if not guild_id:
|
if not guild_id:
|
||||||
return
|
return
|
||||||
|
|
||||||
guild = await self.db.get_guild(guild_id, projection={'votes': 1, 'current_track': 1})
|
guild = await self.db.get_guild(guild_id)
|
||||||
votes = guild['votes']
|
votes = guild['votes']
|
||||||
|
|
||||||
if str(payload.message_id) not in votes:
|
if str(payload.message_id) not in votes:
|
||||||
@@ -158,54 +146,9 @@ class Voice(Cog, VoiceExtension):
|
|||||||
required_votes = 2 if total_members <= 5 else 4 if total_members <= 10 else 6 if total_members <= 15 else 9
|
required_votes = 2 if total_members <= 5 else 4 if total_members <= 10 else 6 if total_members <= 15 else 9
|
||||||
if len(vote_data['positive_votes']) >= required_votes:
|
if len(vote_data['positive_votes']) >= required_votes:
|
||||||
logging.info(f"[VOICE] Enough positive votes for message {payload.message_id}")
|
logging.info(f"[VOICE] Enough positive votes for message {payload.message_id}")
|
||||||
|
await message.delete()
|
||||||
if vote_data['action'] == 'next':
|
await self.proccess_vote(payload, guild, channel, vote_data)
|
||||||
logging.info(f"[VOICE] Skipping track for message {payload.message_id}")
|
del votes[str(payload.message_id)]
|
||||||
|
|
||||||
title = await self.next_track(payload)
|
|
||||||
await message.clear_reactions()
|
|
||||||
await message.edit(content=f"Сейчас играет: **{title}**!", delete_after=15)
|
|
||||||
del votes[str(payload.message_id)]
|
|
||||||
|
|
||||||
elif vote_data['action'] == 'add_track':
|
|
||||||
logging.info(f"[VOICE] Adding track for message {payload.message_id}")
|
|
||||||
await message.clear_reactions()
|
|
||||||
|
|
||||||
track = vote_data['vote_content']
|
|
||||||
if not track:
|
|
||||||
logging.info(f"[VOICE] Recieved empty vote context for message {payload.message_id}")
|
|
||||||
return
|
|
||||||
|
|
||||||
await self.db.modify_track(guild_id, track, 'next', 'append')
|
|
||||||
|
|
||||||
if guild['current_track']:
|
|
||||||
await message.edit(content=f"Трек был добавлен в очередь!", delete_after=15)
|
|
||||||
else:
|
|
||||||
title = await self.next_track(payload)
|
|
||||||
await message.edit(content=f"Сейчас играет: **{title}**!", delete_after=15)
|
|
||||||
|
|
||||||
del votes[str(payload.message_id)]
|
|
||||||
|
|
||||||
elif vote_data['action'] in ('add_album', 'add_artist', 'add_playlist'):
|
|
||||||
logging.info(f"[VOICE] Performing '{vote_data['action']}' action for message {payload.message_id}")
|
|
||||||
|
|
||||||
await message.clear_reactions()
|
|
||||||
|
|
||||||
tracks = vote_data['vote_content']
|
|
||||||
if not tracks:
|
|
||||||
logging.info(f"[VOICE] Recieved empty vote context for message {payload.message_id}")
|
|
||||||
return
|
|
||||||
|
|
||||||
await self.db.update(guild_id, {'is_stopped': False})
|
|
||||||
await self.db.modify_track(guild_id, tracks, 'next', 'extend')
|
|
||||||
|
|
||||||
if guild['current_track']:
|
|
||||||
await message.edit(content=f"Контент был добавлен в очередь!", delete_after=15)
|
|
||||||
else:
|
|
||||||
title = await self.next_track(payload)
|
|
||||||
await message.edit(content=f"Сейчас играет: **{title}**!", delete_after=15)
|
|
||||||
|
|
||||||
del votes[str(payload.message_id)]
|
|
||||||
|
|
||||||
elif len(vote_data['negative_votes']) >= required_votes:
|
elif len(vote_data['negative_votes']) >= required_votes:
|
||||||
logging.info(f"[VOICE] Enough negative votes for message {payload.message_id}")
|
logging.info(f"[VOICE] Enough negative votes for message {payload.message_id}")
|
||||||
@@ -224,9 +167,10 @@ class Voice(Cog, VoiceExtension):
|
|||||||
guild_id = payload.guild_id
|
guild_id = payload.guild_id
|
||||||
if not guild_id:
|
if not guild_id:
|
||||||
return
|
return
|
||||||
|
|
||||||
guild = await self.db.get_guild(guild_id, projection={'votes': 1})
|
guild = await self.db.get_guild(guild_id, projection={'votes': 1})
|
||||||
votes = guild['votes']
|
votes = guild['votes']
|
||||||
|
|
||||||
if str(payload.message_id) not in votes:
|
if str(payload.message_id) not in votes:
|
||||||
logging.info(f"[VOICE] Message {payload.message_id} not found in votes")
|
logging.info(f"[VOICE] Message {payload.message_id} not found in votes")
|
||||||
return
|
return
|
||||||
@@ -257,38 +201,33 @@ class Voice(Cog, VoiceExtension):
|
|||||||
|
|
||||||
await self.db.update(guild_id, {'votes': votes})
|
await self.db.update(guild_id, {'votes': votes})
|
||||||
|
|
||||||
@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 not await self.voice_check(ctx):
|
if await self.voice_check(ctx):
|
||||||
return
|
await self.send_menu_message(ctx)
|
||||||
|
|
||||||
guild = await self.db.get_guild(ctx.guild.id, projection={'always_allow_menu': 1})
|
|
||||||
channel = cast(discord.VoiceChannel, ctx.channel)
|
|
||||||
|
|
||||||
if len(channel.members) > 2 and not guild['always_allow_menu']:
|
|
||||||
logging.info(f"[VOICE] Action declined: other members are present in the voice channel")
|
|
||||||
await ctx.respond("❌ Вы не единственный в голосовом канале.", ephemeral=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
await self.send_menu_message(ctx)
|
|
||||||
|
|
||||||
@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}")
|
||||||
|
|
||||||
member = cast(discord.Member, ctx.author)
|
member = cast(discord.Member, ctx.author)
|
||||||
guild = await self.db.get_guild(ctx.guild.id, projection={'allow_connect': 1})
|
guild = await self.db.get_guild(ctx.guild.id, projection={'allow_change_connect': 1})
|
||||||
|
vc = await self.get_voice_client(ctx)
|
||||||
|
|
||||||
if not member.guild_permissions.manage_channels and not guild['allow_connect']:
|
if not member.guild_permissions.manage_channels and not guild['allow_change_connect']:
|
||||||
response_message = "❌ У вас нет прав для выполнения этой команды."
|
response_message = "❌ У вас нет прав для выполнения этой команды."
|
||||||
elif (vc := await self.get_voice_client(ctx)) and vc.is_connected():
|
elif vc and vc.is_connected():
|
||||||
response_message = "❌ Бот уже находится в голосовом канале. Выключите его с помощью команды /voice leave."
|
response_message = "❌ Бот уже находится в голосовом канале. Выключите его с помощью команды /voice leave."
|
||||||
elif isinstance(ctx.channel, discord.VoiceChannel):
|
elif isinstance(ctx.channel, discord.VoiceChannel):
|
||||||
await ctx.channel.connect(timeout=15)
|
try:
|
||||||
response_message = "Подключение успешно!"
|
await ctx.channel.connect()
|
||||||
|
except TimeoutError:
|
||||||
|
response_message = "❌ Не удалось подключиться к голосовому каналу."
|
||||||
|
else:
|
||||||
|
response_message = "✅ Подключение успешно!"
|
||||||
else:
|
else:
|
||||||
response_message = "❌ Вы должны отправить команду в голосовом канале."
|
response_message = "❌ Вы должны отправить команду в чате голосового канала."
|
||||||
|
|
||||||
logging.info(f"[VOICE] Join command response: {response_message}")
|
logging.info(f"[VOICE] Join command response: {response_message}")
|
||||||
await ctx.respond(response_message, delete_after=15, ephemeral=True)
|
await ctx.respond(response_message, delete_after=15, ephemeral=True)
|
||||||
@@ -298,22 +237,22 @@ class Voice(Cog, VoiceExtension):
|
|||||||
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}")
|
||||||
|
|
||||||
member = cast(discord.Member, ctx.author)
|
member = cast(discord.Member, ctx.author)
|
||||||
guild = await self.db.get_guild(ctx.guild.id, projection={'allow_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_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
|
||||||
|
|
||||||
if (vc := await self.get_voice_client(ctx)) and await self.voice_check(ctx) and vc.is_connected:
|
if (vc := await self.get_voice_client(ctx)) and await self.voice_check(ctx) and vc.is_connected:
|
||||||
res = await self.stop_playing(ctx, full=True)
|
res = await self.stop_playing(ctx, vc=vc, full=True)
|
||||||
if res:
|
if not res:
|
||||||
await vc.disconnect(force=True)
|
|
||||||
await ctx.respond("Отключение успешно!", delete_after=15, ephemeral=True)
|
|
||||||
logging.info(f"[VOICE] Successfully disconnected from voice channel in guild {ctx.guild.id}")
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
await ctx.respond("❌ Не удалось отключиться.", delete_after=15, ephemeral=True)
|
await ctx.respond("❌ Не удалось отключиться.", delete_after=15, ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
await vc.disconnect(force=True)
|
||||||
|
await ctx.respond("✅ Отключение успешно!", delete_after=15, ephemeral=True)
|
||||||
|
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)
|
||||||
|
|
||||||
@@ -329,7 +268,7 @@ class Voice(Cog, VoiceExtension):
|
|||||||
await ctx.respond("❌ У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True)
|
await ctx.respond("❌ У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True)
|
||||||
elif await self.voice_check(ctx):
|
elif await self.voice_check(ctx):
|
||||||
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="Получить очередь треков.")
|
||||||
@@ -346,55 +285,7 @@ class Voice(Cog, VoiceExtension):
|
|||||||
|
|
||||||
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}")
|
||||||
|
|
||||||
@track.command(description="Приостановить текущий трек.")
|
@voice.command(description="Прервать проигрывание, удалить историю, очередь и текущий плеер.")
|
||||||
async def pause(self, ctx: discord.ApplicationContext) -> None:
|
|
||||||
logging.info(f"[VOICE] Pause command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
|
|
||||||
|
|
||||||
member = cast(discord.Member, ctx.author)
|
|
||||||
channel = cast(discord.VoiceChannel, ctx.channel)
|
|
||||||
|
|
||||||
if len(channel.members) > 2 and not member.guild_permissions.manage_channels:
|
|
||||||
logging.info(f"[VOICE] User {ctx.author.id} does not have permissions to pause the track in guild {ctx.guild.id}")
|
|
||||||
await ctx.respond("❌ Вы не можете остановить воспроизведение, пока в канале находятся другие пользователи.", delete_after=15, ephemeral=True)
|
|
||||||
|
|
||||||
elif await self.voice_check(ctx) and (vc := await self.get_voice_client(ctx)) is not None:
|
|
||||||
if not vc.is_paused():
|
|
||||||
vc.pause()
|
|
||||||
|
|
||||||
menu = await self.db.get_current_menu(ctx.guild.id)
|
|
||||||
if menu:
|
|
||||||
await self.update_menu_full(ctx, menu)
|
|
||||||
|
|
||||||
logging.info(f"[VOICE] Track paused in guild {ctx.guild.id}")
|
|
||||||
await ctx.respond("Воспроизведение приостановлено.", delete_after=15, ephemeral=True)
|
|
||||||
else:
|
|
||||||
logging.info(f"[VOICE] Track already paused in guild {ctx.guild.id}")
|
|
||||||
await ctx.respond("Воспроизведение уже приостановлено.", delete_after=15, ephemeral=True)
|
|
||||||
|
|
||||||
@track.command(description="Возобновить текущий трек.")
|
|
||||||
async def resume(self, ctx: discord.ApplicationContext) -> None:
|
|
||||||
logging.info(f"[VOICE] Resume command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
|
|
||||||
|
|
||||||
member = cast(discord.Member, ctx.author)
|
|
||||||
channel = cast(discord.VoiceChannel, ctx.channel)
|
|
||||||
|
|
||||||
if len(channel.members) > 2 and not member.guild_permissions.manage_channels:
|
|
||||||
logging.info(f"[VOICE] User {ctx.author.id} does not have permissions to resume the track in guild {ctx.guild.id}")
|
|
||||||
await ctx.respond("❌ Вы не можете остановить воспроизведение, пока в канале находятся другие пользователи.", delete_after=15, ephemeral=True)
|
|
||||||
|
|
||||||
elif await self.voice_check(ctx) and (vc := await self.get_voice_client(ctx)):
|
|
||||||
if vc.is_paused():
|
|
||||||
vc.resume()
|
|
||||||
menu = await self.db.get_current_menu(ctx.guild.id)
|
|
||||||
if menu:
|
|
||||||
await self.update_menu_full(ctx, menu)
|
|
||||||
logging.info(f"[VOICE] Track resumed in guild {ctx.guild.id}")
|
|
||||||
await ctx.respond("Воспроизведение восстановлено.", delete_after=15, ephemeral=True)
|
|
||||||
else:
|
|
||||||
logging.info(f"[VOICE] Track is not paused in guild {ctx.guild.id}")
|
|
||||||
await ctx.respond("Воспроизведение не на паузе.", delete_after=15, ephemeral=True)
|
|
||||||
|
|
||||||
@track.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}")
|
||||||
|
|
||||||
@@ -408,115 +299,10 @@ class Voice(Cog, VoiceExtension):
|
|||||||
elif await self.voice_check(ctx):
|
elif await self.voice_check(ctx):
|
||||||
res = await self.stop_playing(ctx, full=True)
|
res = await self.stop_playing(ctx, full=True)
|
||||||
if res:
|
if res:
|
||||||
await ctx.respond("Воспроизведение остановлено.", delete_after=15, ephemeral=True)
|
await ctx.respond("✅ Воспроизведение остановлено.", delete_after=15, ephemeral=True)
|
||||||
else:
|
else:
|
||||||
await ctx.respond("❌ Произошла ошибка при остановке воспроизведения.", delete_after=15, ephemeral=True)
|
await ctx.respond("❌ Произошла ошибка при остановке воспроизведения.", delete_after=15, ephemeral=True)
|
||||||
|
|
||||||
@track.command(description="Переключиться на следующую песню в очереди.")
|
|
||||||
async def next(self, ctx: discord.ApplicationContext) -> None:
|
|
||||||
logging.info(f"[VOICE] Next command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
|
|
||||||
if not await self.voice_check(ctx):
|
|
||||||
return
|
|
||||||
|
|
||||||
gid = ctx.guild.id
|
|
||||||
guild = await self.db.get_guild(gid, projection={'next_tracks': 1, 'vote_next_track': 1})
|
|
||||||
if not guild['next_tracks']:
|
|
||||||
logging.info(f"[VOICE] No tracks in queue in guild {ctx.guild.id}")
|
|
||||||
await ctx.respond("❌ Нет песенен в очереди.", delete_after=15, ephemeral=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
member = cast(discord.Member, ctx.author)
|
|
||||||
channel = cast(discord.VoiceChannel, ctx.channel)
|
|
||||||
|
|
||||||
if guild['vote_next_track'] and len(channel.members) > 2 and not member.guild_permissions.manage_channels:
|
|
||||||
logging.info(f"[VOICE] User {ctx.author.id} started vote to skip track in guild {ctx.guild.id}")
|
|
||||||
|
|
||||||
message = cast(discord.Interaction, await ctx.respond(f"{member.mention} хочет пропустить текущий трек.\n\nВыполнить переход?", delete_after=30))
|
|
||||||
response = await message.original_response()
|
|
||||||
|
|
||||||
await response.add_reaction('✅')
|
|
||||||
await response.add_reaction('❌')
|
|
||||||
|
|
||||||
await self.db.update_vote(
|
|
||||||
gid,
|
|
||||||
response.id,
|
|
||||||
{
|
|
||||||
'positive_votes': list(),
|
|
||||||
'negative_votes': list(),
|
|
||||||
'total_members': len(channel.members),
|
|
||||||
'action': 'next',
|
|
||||||
'vote_content': None
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logging.info(f"[VOICE] Skipping vote for user {ctx.author.id} in guild {ctx.guild.id}")
|
|
||||||
|
|
||||||
await self.db.update(gid, {'is_stopped': False})
|
|
||||||
title = await self.next_track(ctx)
|
|
||||||
await ctx.respond(f"Сейчас играет: **{title}**!", delete_after=15)
|
|
||||||
|
|
||||||
@track.command(description="Добавить трек в избранное или убрать, если он уже там.")
|
|
||||||
async def like(self, ctx: discord.ApplicationContext) -> None:
|
|
||||||
logging.info(f"[VOICE] Like command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
|
|
||||||
|
|
||||||
if not await self.voice_check(ctx):
|
|
||||||
return
|
|
||||||
|
|
||||||
vc = await self.get_voice_client(ctx)
|
|
||||||
if not vc or not vc.is_playing:
|
|
||||||
logging.info(f"[VOICE] No current track in {ctx.guild.id}")
|
|
||||||
await ctx.respond("Нет воспроизводимого трека.", delete_after=15, ephemeral=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
result = await self.react_track(ctx, 'like')
|
|
||||||
if not result[0]:
|
|
||||||
logging.warning(f"Like command failed for user {ctx.author.id} in guild {ctx.guild.id}")
|
|
||||||
await ctx.respond("❌ Операция не удалась.", delete_after=15, ephemeral=True)
|
|
||||||
elif result[1] == 'removed':
|
|
||||||
logging.info(f"[VOICE] Track removed from favorites for user {ctx.author.id} in guild {ctx.guild.id}")
|
|
||||||
await ctx.respond("Трек был удалён из избранного.", delete_after=15, ephemeral=True)
|
|
||||||
elif result[1] == 'added':
|
|
||||||
logging.info(f"[VOICE] Track added to favorites for user {ctx.author.id} in guild {ctx.guild.id}")
|
|
||||||
await ctx.respond(f"Трек **{result}** был добавлен в избранное.", delete_after=15, ephemeral=True)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown like command result: '{result}'")
|
|
||||||
|
|
||||||
@track.command(name='vibe', description="Запустить Мою Волну по текущему треку.")
|
|
||||||
async def track_vibe(self, ctx: discord.ApplicationContext) -> None:
|
|
||||||
logging.info(f"[VOICE] Vibe (track) command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
|
|
||||||
if not await self.voice_check(ctx):
|
|
||||||
return
|
|
||||||
|
|
||||||
guild = await self.db.get_guild(ctx.guild.id, projection={'always_allow_menu': 1, 'current_track': 1, 'current_menu': 1, 'vibing': 1})
|
|
||||||
channel = cast(discord.VoiceChannel, ctx.channel)
|
|
||||||
|
|
||||||
if len(channel.members) > 2 and not guild['always_allow_menu']:
|
|
||||||
logging.info(f"[VOICE] Action declined: other members are present in the voice channel")
|
|
||||||
await ctx.respond("❌ Вы не единственный в голосовом канале.", ephemeral=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
if guild['vibing']:
|
|
||||||
logging.info(f"[VOICE] Action declined: vibing is already enabled in guild {ctx.guild.id}")
|
|
||||||
await ctx.respond("❌ Моя Волна уже включена. Используйте /track stop, чтобы остановить воспроизведение.", ephemeral=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
if not guild['current_track']:
|
|
||||||
logging.info(f"[VOICE] No current track in {ctx.guild.id}")
|
|
||||||
await ctx.respond("❌ Нет воспроизводимого трека.", ephemeral=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
feedback = await self.update_vibe(ctx, 'track', guild['current_track']['id'])
|
|
||||||
if not feedback:
|
|
||||||
await ctx.respond("❌ Операция не удалась. Возможно, у вес нет подписки на Яндекс Музыку.", ephemeral=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
if not guild['current_menu']:
|
|
||||||
await self.send_menu_message(ctx, disable=True)
|
|
||||||
|
|
||||||
next_track = await self.db.get_track(ctx.guild_id, 'next')
|
|
||||||
if next_track:
|
|
||||||
await self._play_next_track(ctx, next_track)
|
|
||||||
|
|
||||||
@voice.command(name='vibe', description="Запустить Мою Волну.")
|
@voice.command(name='vibe', description="Запустить Мою Волну.")
|
||||||
@discord.option(
|
@discord.option(
|
||||||
"запрос",
|
"запрос",
|
||||||
@@ -531,18 +317,14 @@ class Voice(Cog, VoiceExtension):
|
|||||||
if not await self.voice_check(ctx):
|
if not await self.voice_check(ctx):
|
||||||
return
|
return
|
||||||
|
|
||||||
guild = await self.db.get_guild(ctx.guild.id, projection={'always_allow_menu': 1, 'current_menu': 1, 'vibing': 1})
|
guild = await self.db.get_guild(ctx.guild.id, projection={'current_menu': 1, 'vibing': 1})
|
||||||
channel = cast(discord.VoiceChannel, ctx.channel)
|
|
||||||
|
|
||||||
if len(channel.members) > 2 and not guild['always_allow_menu']:
|
|
||||||
logging.info(f"[VOICE] Action declined: other members are present in the voice channel")
|
|
||||||
await ctx.respond("❌ Вы не единственный в голосовом канале.", ephemeral=True)
|
|
||||||
return
|
|
||||||
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("❌ Моя Волна уже включена. Используйте /track stop, чтобы остановить воспроизведение.", ephemeral=True)
|
await ctx.respond("❌ Моя Волна уже включена. Используйте /track stop, чтобы остановить воспроизведение.", delete_after=15, ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
await ctx.defer(invisible=False)
|
||||||
if name:
|
if name:
|
||||||
token = await users_db.get_ym_token(ctx.user.id)
|
token = await users_db.get_ym_token(ctx.user.id)
|
||||||
if not token:
|
if not token:
|
||||||
@@ -564,27 +346,29 @@ class Voice(Cog, VoiceExtension):
|
|||||||
|
|
||||||
if not content:
|
if not content:
|
||||||
logging.debug(f"[VOICE] Station {name} not found")
|
logging.debug(f"[VOICE] Station {name} not found")
|
||||||
await ctx.respond("❌ Станция не найдена.", ephemeral=True)
|
await ctx.respond("❌ Станция не найдена.", delete_after=15, ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
_type, _id = content.ad_params.other_params.split(':') if content.ad_params else (None, None)
|
_type, _id = content.ad_params.other_params.split(':') if content.ad_params else (None, None)
|
||||||
|
|
||||||
if not _type or not _id:
|
if not _type or not _id:
|
||||||
logging.debug(f"[VOICE] Station {name} has no ad params")
|
logging.debug(f"[VOICE] Station {name} has no ad params")
|
||||||
await ctx.respond("❌ Станция не найдена.", ephemeral=True)
|
await ctx.respond("❌ Станция не найдена.", delete_after=15, ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
feedback = await self.update_vibe(ctx, _type, _id)
|
|
||||||
else:
|
else:
|
||||||
feedback = await self.update_vibe(ctx, 'user', 'onyourwave')
|
_type, _id = 'user', 'onyourwave'
|
||||||
|
|
||||||
|
feedback = await self.update_vibe(ctx, _type, _id)
|
||||||
|
|
||||||
if not feedback:
|
if not feedback:
|
||||||
await ctx.respond("❌ Операция не удалась. Возможно, у вес нет подписки на Яндекс Музыку.", ephemeral=True)
|
await ctx.respond("❌ Операция не удалась. Возможно, у вес нет подписки на Яндекс Музыку.", delete_after=15, ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not guild['current_menu']:
|
if guild['current_menu']:
|
||||||
|
await ctx.respond("✅ Моя Волна включена.", delete_after=15, ephemeral=True)
|
||||||
|
else:
|
||||||
await self.send_menu_message(ctx, disable=True)
|
await self.send_menu_message(ctx, disable=True)
|
||||||
|
|
||||||
next_track = await self.db.get_track(ctx.guild_id, 'next')
|
next_track = await self.db.get_track(ctx.guild_id, 'next')
|
||||||
if next_track:
|
if next_track:
|
||||||
await self._play_next_track(ctx, next_track)
|
await self._play_track(ctx, next_track)
|
||||||
|
|||||||
@@ -80,12 +80,9 @@ class BaseGuildsDatabase:
|
|||||||
current_menu=None,
|
current_menu=None,
|
||||||
is_stopped=True,
|
is_stopped=True,
|
||||||
always_allow_menu=False,
|
always_allow_menu=False,
|
||||||
allow_connect=True,
|
allow_change_connect=True,
|
||||||
vote_next_track=True,
|
vote_switch_track=True,
|
||||||
vote_add_track=True,
|
vote_add=True,
|
||||||
vote_add_album=True,
|
|
||||||
vote_add_artist=True,
|
|
||||||
vote_add_playlist=True,
|
|
||||||
shuffle=False,
|
shuffle=False,
|
||||||
repeat=False,
|
repeat=False,
|
||||||
votes={},
|
votes={},
|
||||||
|
|||||||
@@ -19,28 +19,29 @@ class VoiceGuildsDatabase(BaseGuildsDatabase):
|
|||||||
if list_type not in ('next', 'previous', 'current'):
|
if list_type not in ('next', 'previous', 'current'):
|
||||||
raise ValueError("list_type must be either 'next' or 'previous'")
|
raise ValueError("list_type must be either 'next' or 'previous'")
|
||||||
|
|
||||||
if list_type == 'current':
|
|
||||||
return (await self.get_guild(gid, projection={'current_track': 1}))['current_track']
|
|
||||||
|
|
||||||
field = f'{list_type}_tracks'
|
field = f'{list_type}_tracks'
|
||||||
update = {'$pop': {field: -1}}
|
guild = await self.get_guild(gid, projection={'current_track': 1, field: 1})
|
||||||
|
|
||||||
|
if list_type == 'current':
|
||||||
|
return guild['current_track']
|
||||||
|
|
||||||
result = await guilds.find_one_and_update(
|
result = await guilds.find_one_and_update(
|
||||||
{'_id': gid},
|
{'_id': gid},
|
||||||
update,
|
{'$pop': {field: -1}},
|
||||||
projection={field: 1},
|
projection={field: 1},
|
||||||
return_document=ReturnDocument.BEFORE
|
return_document=ReturnDocument.BEFORE
|
||||||
)
|
)
|
||||||
|
|
||||||
res = result.get(field, [])[0] if result and result.get(field) else None
|
res = result.get(field, []) if result else None
|
||||||
|
|
||||||
if field == 'previous_tracks' and res:
|
if field == 'previous_tracks' and res:
|
||||||
await guilds.find_one_and_update(
|
await guilds.find_one_and_update(
|
||||||
{'_id': gid},
|
{'_id': gid},
|
||||||
{'$push': {'next_tracks': {'$each': [res], '$position': 0}}},
|
{'$push': {'next_tracks': {'$each': [guild['current_track']], '$position': 0}}},
|
||||||
projection={'next_tracks': 1}
|
projection={'next_tracks': 1}
|
||||||
)
|
)
|
||||||
|
|
||||||
return res
|
return res[0] if res else None
|
||||||
|
|
||||||
async def modify_track(
|
async def modify_track(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ class MessageVotes(TypedDict):
|
|||||||
positive_votes: list[int]
|
positive_votes: list[int]
|
||||||
negative_votes: list[int]
|
negative_votes: list[int]
|
||||||
total_members: int
|
total_members: int
|
||||||
action: Literal['next', 'add_track', 'add_album', 'add_artist', 'add_playlist']
|
action: Literal['next', 'play/pause', 'repeat', 'shuffle', 'previous', 'add_track', 'add_album', 'add_artist', 'add_playlist']
|
||||||
vote_content: dict[str, Any] | list[dict[str, Any]] | None
|
vote_content: Any | None
|
||||||
|
|
||||||
class Guild(TypedDict, total=False):
|
class Guild(TypedDict, total=False):
|
||||||
next_tracks: list[dict[str, Any]]
|
next_tracks: list[dict[str, Any]]
|
||||||
@@ -14,12 +14,9 @@ class Guild(TypedDict, total=False):
|
|||||||
current_menu: int | None
|
current_menu: int | None
|
||||||
is_stopped: bool
|
is_stopped: bool
|
||||||
always_allow_menu: bool
|
always_allow_menu: bool
|
||||||
allow_connect: bool
|
allow_change_connect: bool
|
||||||
vote_next_track: bool
|
vote_switch_track: bool
|
||||||
vote_add_track: bool
|
vote_add: bool
|
||||||
vote_add_album: bool
|
|
||||||
vote_add_artist: bool
|
|
||||||
vote_add_playlist: bool
|
|
||||||
shuffle: bool
|
shuffle: bool
|
||||||
repeat: bool
|
repeat: bool
|
||||||
votes: dict[str, MessageVotes]
|
votes: dict[str, MessageVotes]
|
||||||
@@ -34,12 +31,9 @@ class ExplicitGuild(TypedDict):
|
|||||||
current_menu: int | None
|
current_menu: int | None
|
||||||
is_stopped: bool # Prevents the `after` callback of play_track
|
is_stopped: bool # Prevents the `after` callback of play_track
|
||||||
always_allow_menu: bool
|
always_allow_menu: bool
|
||||||
allow_connect: bool
|
allow_change_connect: bool
|
||||||
vote_next_track: bool
|
vote_switch_track: bool
|
||||||
vote_add_track: bool
|
vote_add: bool
|
||||||
vote_add_album: bool
|
|
||||||
vote_add_artist: bool
|
|
||||||
vote_add_playlist: bool
|
|
||||||
shuffle: bool
|
shuffle: bool
|
||||||
repeat: bool
|
repeat: bool
|
||||||
votes: dict[str, MessageVotes]
|
votes: dict[str, MessageVotes]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any, Literal, cast
|
from typing import cast
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from yandex_music import Track, Album, Artist, Playlist
|
from yandex_music import Track, Album, Artist, Playlist
|
||||||
@@ -27,40 +27,39 @@ class PlayButton(Button, VoiceExtension):
|
|||||||
return
|
return
|
||||||
|
|
||||||
gid = interaction.guild.id
|
gid = interaction.guild.id
|
||||||
guild = await self.db.get_guild(gid, projection={'current_track': 1, 'current_menu': 1, 'vote_add_track': 1, 'vote_add_album': 1, 'vote_add_artist': 1, 'vote_add_playlist': 1})
|
guild = await self.db.get_guild(gid, projection={'current_track': 1, 'current_menu': 1, 'vote_add': 1})
|
||||||
channel = cast(discord.VoiceChannel, interaction.channel)
|
channel = cast(discord.VoiceChannel, interaction.channel)
|
||||||
member = cast(discord.Member, interaction.user)
|
member = cast(discord.Member, interaction.user)
|
||||||
action: Literal['add_track', 'add_album', 'add_artist', 'add_playlist']
|
|
||||||
|
|
||||||
if isinstance(self.item, Track):
|
if isinstance(self.item, Track):
|
||||||
tracks = [self.item]
|
tracks = [self.item]
|
||||||
action = 'add_track'
|
action = 'add_track'
|
||||||
vote_message = f"{member.mention} хочет добавить трек **{self.item.title}** в очередь.\n\n Голосуйте за добавление."
|
vote_message = f"{member.mention} хочет добавить трек **{self.item.title}** в очередь.\n\n Голосуйте за добавление."
|
||||||
response_message = f"Трек **{self.item.title}** был добавлен в очередь."
|
response_message = f"✅ Трек **{self.item.title}** был добавлен в очередь."
|
||||||
|
|
||||||
elif isinstance(self.item, Album):
|
elif isinstance(self.item, Album):
|
||||||
album = await self.item.with_tracks_async()
|
album = await self.item.with_tracks_async()
|
||||||
if not album or not album.volumes:
|
if not album or not album.volumes:
|
||||||
logging.debug("[FIND] Failed to fetch album tracks in PlayButton callback")
|
logging.debug("[FIND] Failed to fetch album tracks in PlayButton callback")
|
||||||
await interaction.respond("Не удалось получить треки альбома.", ephemeral=True)
|
await interaction.respond("❌ Не удалось получить треки альбома.", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
tracks = [track for volume in album.volumes for track in volume]
|
tracks = [track for volume in album.volumes for track in volume]
|
||||||
action = 'add_album'
|
action = 'add_album'
|
||||||
vote_message = f"{member.mention} хочет добавить альбом **{self.item.title}** в очередь.\n\n Голосуйте за добавление."
|
vote_message = f"{member.mention} хочет добавить альбом **{self.item.title}** в очередь.\n\n Голосуйте за добавление."
|
||||||
response_message = f"Альбом **{self.item.title}** был добавлен в очередь."
|
response_message = f"✅ Альбом **{self.item.title}** был добавлен в очередь."
|
||||||
|
|
||||||
elif isinstance(self.item, Artist):
|
elif isinstance(self.item, Artist):
|
||||||
artist_tracks = await self.item.get_tracks_async()
|
artist_tracks = await self.item.get_tracks_async()
|
||||||
if not artist_tracks:
|
if not artist_tracks:
|
||||||
logging.debug("[FIND] Failed to fetch artist tracks in PlayButton callback")
|
logging.debug("[FIND] Failed to fetch artist tracks in PlayButton callback")
|
||||||
await interaction.respond("Не удалось получить треки артиста.", ephemeral=True)
|
await interaction.respond("❌ Не удалось получить треки артиста.", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
tracks = artist_tracks.tracks.copy()
|
tracks = artist_tracks.tracks.copy()
|
||||||
action = 'add_artist'
|
action = 'add_artist'
|
||||||
vote_message = f"{member.mention} хочет добавить треки от **{self.item.name}** в очередь.\n\n Голосуйте за добавление."
|
vote_message = f"{member.mention} хочет добавить треки от **{self.item.name}** в очередь.\n\n Голосуйте за добавление."
|
||||||
response_message = f"Песни артиста **{self.item.name}** были добавлены в очередь."
|
response_message = f"✅ Песни артиста **{self.item.name}** были добавлены в очередь."
|
||||||
|
|
||||||
elif isinstance(self.item, Playlist):
|
elif isinstance(self.item, Playlist):
|
||||||
short_tracks = await self.item.fetch_tracks_async()
|
short_tracks = await self.item.fetch_tracks_async()
|
||||||
@@ -72,7 +71,7 @@ class PlayButton(Button, VoiceExtension):
|
|||||||
tracks = [cast(Track, short_track.track) for short_track in short_tracks]
|
tracks = [cast(Track, short_track.track) for short_track in short_tracks]
|
||||||
action = 'add_playlist'
|
action = 'add_playlist'
|
||||||
vote_message = f"{member.mention} хочет добавить плейлист **{self.item.title}** в очередь.\n\n Голосуйте за добавление."
|
vote_message = f"{member.mention} хочет добавить плейлист **{self.item.title}** в очередь.\n\n Голосуйте за добавление."
|
||||||
response_message = f"Плейлист **{self.item.title}** был добавлен в очередь."
|
response_message = f"✅ Плейлист **{self.item.title}** был добавлен в очередь."
|
||||||
|
|
||||||
elif isinstance(self.item, list):
|
elif isinstance(self.item, list):
|
||||||
tracks = self.item.copy()
|
tracks = self.item.copy()
|
||||||
@@ -83,12 +82,12 @@ class PlayButton(Button, VoiceExtension):
|
|||||||
|
|
||||||
action = 'add_playlist'
|
action = 'add_playlist'
|
||||||
vote_message = f"{member.mention} хочет добавить плейлист **Мне Нравится** в очередь.\n\n Голосуйте за добавление."
|
vote_message = f"{member.mention} хочет добавить плейлист **Мне Нравится** в очередь.\n\n Голосуйте за добавление."
|
||||||
response_message = f"Плейлист **«Мне нравится»** был добавлен в очередь."
|
response_message = f"✅ Плейлист **«Мне нравится»** был добавлен в очередь."
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unknown item type: '{type(self.item).__name__}'")
|
raise ValueError(f"Unknown item type: '{type(self.item).__name__}'")
|
||||||
|
|
||||||
if guild.get(f'vote_{action}') and len(channel.members) > 2 and not member.guild_permissions.manage_channels:
|
if guild['vote_add'] and len(channel.members) > 2 and not member.guild_permissions.manage_channels:
|
||||||
logging.debug(f"Starting vote for '{action}' (from PlayButton callback)")
|
logging.debug(f"Starting vote for '{action}' (from PlayButton callback)")
|
||||||
|
|
||||||
message = cast(discord.Interaction, await interaction.respond(vote_message, delete_after=30))
|
message = cast(discord.Interaction, await interaction.respond(vote_message, delete_after=30))
|
||||||
@@ -108,26 +107,28 @@ class PlayButton(Button, VoiceExtension):
|
|||||||
'vote_content': [track.to_dict() for track in tracks]
|
'vote_content': [track.to_dict() for track in tracks]
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
logging.debug(f"[FIND] Skipping vote for '{action}'")
|
||||||
|
|
||||||
|
if guild['current_menu']:
|
||||||
|
await interaction.respond(response_message, delete_after=15)
|
||||||
else:
|
else:
|
||||||
logging.debug(f"[FIND] Skipping vote for '{action}' (from PlayButton callback)")
|
await self.send_menu_message(interaction, disable=True)
|
||||||
|
|
||||||
current_menu = await self.get_menu_message(interaction, guild['current_menu']) if guild['current_menu'] else None
|
if guild['current_track'] is not None:
|
||||||
|
logging.debug(f"[FIND] Adding tracks to queue")
|
||||||
|
await self.db.modify_track(gid, tracks, 'next', 'extend')
|
||||||
|
else:
|
||||||
|
logging.debug(f"[FIND] Playing track")
|
||||||
|
track = tracks.pop(0)
|
||||||
|
await self.db.modify_track(gid, tracks, 'next', 'extend')
|
||||||
|
await self.play_track(interaction, track)
|
||||||
|
|
||||||
if guild['current_track'] is not None:
|
if interaction.message:
|
||||||
logging.debug(f"[FIND] Adding tracks to queue (from PlayButton callback)")
|
await interaction.message.delete()
|
||||||
await self.db.modify_track(gid, tracks, 'next', 'extend')
|
else:
|
||||||
else:
|
logging.warning(f"[FIND] Interaction message is None")
|
||||||
logging.debug(f"[FIND] Playing track (from PlayButton callback)")
|
|
||||||
track = tracks.pop(0)
|
|
||||||
await self.db.modify_track(gid, tracks, 'next', 'extend')
|
|
||||||
await self.play_track(interaction, track)
|
|
||||||
response_message = f"Сейчас играет: **{track.title}**!"
|
|
||||||
|
|
||||||
if current_menu and interaction.message:
|
|
||||||
logging.debug(f"[FIND] Deleting interaction message {interaction.message.id}: current player {current_menu.id} found")
|
|
||||||
await interaction.message.delete()
|
|
||||||
else:
|
|
||||||
await interaction.respond(response_message, delete_after=15)
|
|
||||||
|
|
||||||
class MyVibeButton(Button, VoiceExtension):
|
class MyVibeButton(Button, VoiceExtension):
|
||||||
def __init__(self, item: Track | Album | Artist | Playlist | list[Track], *args, **kwargs):
|
def __init__(self, item: Track | Album | Artist | Playlist | list[Track], *args, **kwargs):
|
||||||
@@ -145,14 +146,6 @@ class MyVibeButton(Button, VoiceExtension):
|
|||||||
logging.warning(f"[VIBE] Guild ID is None in button callback")
|
logging.warning(f"[VIBE] Guild ID is None in button callback")
|
||||||
return
|
return
|
||||||
|
|
||||||
guild = await self.db.get_guild(gid)
|
|
||||||
channel = cast(discord.VoiceChannel, interaction.channel)
|
|
||||||
|
|
||||||
if len(channel.members) > 2 and not guild['always_allow_menu']:
|
|
||||||
logging.info(f"[VIBE] Button callback declined: other members are present in the voice channel")
|
|
||||||
await interaction.respond("❌ Вы не единственный в голосовом канале.", ephemeral=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
track_type_map = {
|
track_type_map = {
|
||||||
Track: 'track', Album: 'album', Artist: 'artist', Playlist: 'playlist', list: 'user'
|
Track: 'track', Album: 'album', Artist: 'artist', Playlist: 'playlist', list: 'user'
|
||||||
}
|
}
|
||||||
@@ -178,7 +171,7 @@ class MyVibeButton(Button, VoiceExtension):
|
|||||||
|
|
||||||
next_track = await self.db.get_track(gid, 'next')
|
next_track = await self.db.get_track(gid, 'next')
|
||||||
if next_track:
|
if next_track:
|
||||||
await self._play_next_track(interaction, next_track)
|
await self._play_track(interaction, next_track)
|
||||||
|
|
||||||
class ListenView(View):
|
class ListenView(View):
|
||||||
def __init__(self, item: Track | Album | Artist | Playlist | list[Track], *items: Item, timeout: float | None = 360, disable_on_timeout: bool = True):
|
def __init__(self, item: Track | Album | Artist | Playlist | list[Track], *items: Item, timeout: float | None = 360, disable_on_timeout: bool = True):
|
||||||
@@ -217,3 +210,4 @@ class ListenView(View):
|
|||||||
return await super().on_timeout()
|
return await super().on_timeout()
|
||||||
except discord.NotFound:
|
except discord.NotFound:
|
||||||
pass
|
pass
|
||||||
|
self.stop()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import logging
|
|||||||
from typing import Self, cast
|
from typing import Self, cast
|
||||||
|
|
||||||
from discord.ui import View, Button, Item, Select
|
from discord.ui import View, Button, Item, Select
|
||||||
from discord import VoiceChannel, ButtonStyle, Interaction, ApplicationContext, RawReactionActionEvent, Embed, ComponentType, SelectOption
|
from discord import VoiceChannel, ButtonStyle, Interaction, ApplicationContext, RawReactionActionEvent, Embed, ComponentType, SelectOption, Member
|
||||||
|
|
||||||
import yandex_music.exceptions
|
import yandex_music.exceptions
|
||||||
from yandex_music import TrackLyrics, Playlist, ClientAsync as YMClient
|
from yandex_music import TrackLyrics, Playlist, ClientAsync as YMClient
|
||||||
@@ -13,14 +13,14 @@ class ToggleButton(Button, VoiceExtension):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
VoiceExtension.__init__(self, None)
|
VoiceExtension.__init__(self, None)
|
||||||
|
|
||||||
async def callback(self, interaction: Interaction):
|
async def callback(self, interaction: Interaction) -> None:
|
||||||
callback_type = interaction.custom_id
|
callback_type = interaction.custom_id
|
||||||
if callback_type not in ('repeat', 'shuffle'):
|
if callback_type not in ('repeat', 'shuffle'):
|
||||||
raise ValueError(f"Invalid callback type: '{callback_type}'")
|
raise ValueError(f"Invalid callback type: '{callback_type}'")
|
||||||
|
|
||||||
logging.info(f'[MENU] {callback_type.capitalize()} button callback')
|
logging.info(f'[MENU] {callback_type.capitalize()} button callback')
|
||||||
|
|
||||||
if not (gid := interaction.guild_id):
|
if not (gid := interaction.guild_id) or not interaction.user:
|
||||||
logging.warning('[MENU] Failed to get guild ID.')
|
logging.warning('[MENU] Failed to get guild ID.')
|
||||||
await interaction.respond("❌ Что-то пошло не так. Попробуйте снова.", delete_after=15, ephemeral=True)
|
await interaction.respond("❌ Что-то пошло не так. Попробуйте снова.", delete_after=15, ephemeral=True)
|
||||||
return
|
return
|
||||||
@@ -29,9 +29,36 @@ class ToggleButton(Button, VoiceExtension):
|
|||||||
return
|
return
|
||||||
|
|
||||||
guild = await self.db.get_guild(gid)
|
guild = await self.db.get_guild(gid)
|
||||||
|
member = cast(Member, interaction.user)
|
||||||
|
channel = cast(VoiceChannel, interaction.channel)
|
||||||
|
|
||||||
|
if len(channel.members) > 2 and not member.guild_permissions.manage_channels:
|
||||||
|
logging.info(f"[MENU] User {interaction.user.id} started vote to pause/resume track in guild {gid}")
|
||||||
|
|
||||||
|
action = "выключить" if guild[callback_type] else "включить"
|
||||||
|
task = "перемешивание треков" if callback_type == 'shuffle' else "повтор трека"
|
||||||
|
message = cast(Interaction, await interaction.respond(f"{member.mention} хочет {action} {task}.\n\nВыполнить действие?", delete_after=60))
|
||||||
|
response = await message.original_response()
|
||||||
|
|
||||||
|
await response.add_reaction('✅')
|
||||||
|
await response.add_reaction('❌')
|
||||||
|
|
||||||
|
await self.db.update_vote(
|
||||||
|
gid,
|
||||||
|
response.id,
|
||||||
|
{
|
||||||
|
'positive_votes': list(),
|
||||||
|
'negative_votes': list(),
|
||||||
|
'total_members': len(channel.members),
|
||||||
|
'action': callback_type,
|
||||||
|
'vote_content': None
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
await self.db.update(gid, {callback_type: not guild[callback_type]})
|
await self.db.update(gid, {callback_type: not guild[callback_type]})
|
||||||
|
|
||||||
if not await self.update_menu_view(interaction, guild, button_callback=True):
|
if not await self.update_menu_view(interaction, button_callback=True):
|
||||||
await interaction.respond("❌ Что-то пошло не так. Попробуйте снова.", delete_after=15, ephemeral=True)
|
await interaction.respond("❌ Что-то пошло не так. Попробуйте снова.", delete_after=15, ephemeral=True)
|
||||||
|
|
||||||
class PlayPauseButton(Button, VoiceExtension):
|
class PlayPauseButton(Button, VoiceExtension):
|
||||||
@@ -44,9 +71,39 @@ class PlayPauseButton(Button, VoiceExtension):
|
|||||||
if not await self.voice_check(interaction, check_vibe_privilage=True):
|
if not await self.voice_check(interaction, check_vibe_privilage=True):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not (gid := interaction.guild_id) or not interaction.user:
|
||||||
|
logging.warning('[MENU] Failed to get guild ID or user.')
|
||||||
|
return
|
||||||
|
|
||||||
if not (vc := await self.get_voice_client(interaction)) or not interaction.message:
|
if not (vc := await self.get_voice_client(interaction)) or not interaction.message:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
member = cast(Member, interaction.user)
|
||||||
|
channel = cast(VoiceChannel, interaction.channel)
|
||||||
|
|
||||||
|
if len(channel.members) > 2 and not member.guild_permissions.manage_channels:
|
||||||
|
logging.info(f"[MENU] User {interaction.user.id} started vote to pause/resume track in guild {gid}")
|
||||||
|
|
||||||
|
task = "приостановить" if vc.is_playing() else "возобновить"
|
||||||
|
message = cast(Interaction, await interaction.respond(f"{member.mention} хочет {task} проигрывание.\n\nВыполнить действие?", delete_after=60))
|
||||||
|
response = await message.original_response()
|
||||||
|
|
||||||
|
await response.add_reaction('✅')
|
||||||
|
await response.add_reaction('❌')
|
||||||
|
|
||||||
|
await self.db.update_vote(
|
||||||
|
gid,
|
||||||
|
response.id,
|
||||||
|
{
|
||||||
|
'positive_votes': list(),
|
||||||
|
'negative_votes': list(),
|
||||||
|
'total_members': len(channel.members),
|
||||||
|
'action': "play/pause",
|
||||||
|
'vote_content': None
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
embed = interaction.message.embeds[0]
|
embed = interaction.message.embeds[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
@@ -67,23 +124,61 @@ class SwitchTrackButton(Button, VoiceExtension):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
VoiceExtension.__init__(self, None)
|
VoiceExtension.__init__(self, None)
|
||||||
|
|
||||||
async def callback(self, interaction: Interaction):
|
async def callback(self, interaction: Interaction) -> None:
|
||||||
callback_type = interaction.custom_id
|
callback_type = interaction.custom_id
|
||||||
if callback_type not in ('next', 'previous'):
|
if callback_type not in ('next', 'previous'):
|
||||||
raise ValueError(f"Invalid callback type: '{callback_type}'")
|
raise ValueError(f"Invalid callback type: '{callback_type}'")
|
||||||
|
|
||||||
|
if not (gid := interaction.guild_id) or not interaction.user:
|
||||||
|
logging.warning(f"[MENU] {callback_type.capitalize()} track button callback without guild id or user")
|
||||||
|
return
|
||||||
|
|
||||||
logging.info(f'[MENU] {callback_type.capitalize()} track button callback')
|
logging.info(f'[MENU] {callback_type.capitalize()} track button callback')
|
||||||
|
|
||||||
if not await self.voice_check(interaction, check_vibe_privilage=True):
|
if not await self.voice_check(interaction, check_vibe_privilage=True):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
tracks_type = callback_type + '_tracks'
|
||||||
|
guild = await self.db.get_guild(gid, projection={tracks_type: 1, 'vote_switch_track': 1})
|
||||||
|
|
||||||
|
if not guild[tracks_type]:
|
||||||
|
logging.info(f"[MENU] No tracks in '{tracks_type}' list in guild {gid}")
|
||||||
|
await interaction.respond(f"❌ Нет треков в {'очереди' if callback_type == 'next' else 'истории'}.", delete_after=15, ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
member = cast(Member, interaction.user)
|
||||||
|
channel = cast(VoiceChannel, interaction.channel)
|
||||||
|
|
||||||
|
if guild['vote_switch_track'] and len(channel.members) > 2 and not member.guild_permissions.manage_channels:
|
||||||
|
logging.info(f"[MENU] User {interaction.user.id} started vote to skip track in guild {gid}")
|
||||||
|
|
||||||
|
task = "пропустить текущий трек" if callback_type == 'next' else "вернуться к предыдущему треку"
|
||||||
|
message = cast(Interaction, await interaction.respond(f"{member.mention} хочет {task}.\n\nВыполнить переход?", delete_after=60))
|
||||||
|
response = await message.original_response()
|
||||||
|
|
||||||
|
await response.add_reaction('✅')
|
||||||
|
await response.add_reaction('❌')
|
||||||
|
|
||||||
|
await self.db.update_vote(
|
||||||
|
gid,
|
||||||
|
response.id,
|
||||||
|
{
|
||||||
|
'positive_votes': list(),
|
||||||
|
'negative_votes': list(),
|
||||||
|
'total_members': len(channel.members),
|
||||||
|
'action': callback_type,
|
||||||
|
'vote_content': None
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
if callback_type == 'next':
|
if callback_type == 'next':
|
||||||
title = await self.next_track(interaction, button_callback=True)
|
title = await self.next_track(interaction, button_callback=True)
|
||||||
else:
|
else:
|
||||||
title = await self.prev_track(interaction, button_callback=True)
|
title = await self.previous_track(interaction, button_callback=True)
|
||||||
|
|
||||||
if not title:
|
if not title:
|
||||||
await interaction.respond(f"❌ Нет треков в очереди.", delete_after=15, ephemeral=True)
|
await interaction.respond(f"❌ Что-то пошло не так. Попробуйте позже.", delete_after=15, ephemeral=True)
|
||||||
|
|
||||||
class ReactionButton(Button, VoiceExtension):
|
class ReactionButton(Button, VoiceExtension):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -103,16 +198,32 @@ class ReactionButton(Button, VoiceExtension):
|
|||||||
if not (vc := await self.get_voice_client(interaction)) or not vc.is_playing:
|
if not (vc := await self.get_voice_client(interaction)) or not vc.is_playing:
|
||||||
await interaction.respond("❌ Нет воспроизводимого трека.", delete_after=15, ephemeral=True)
|
await interaction.respond("❌ Нет воспроизводимого трека.", delete_after=15, ephemeral=True)
|
||||||
|
|
||||||
|
channel = cast(VoiceChannel, interaction.channel)
|
||||||
res = await self.react_track(interaction, callback_type)
|
res = await self.react_track(interaction, callback_type)
|
||||||
|
|
||||||
if callback_type == 'like' and res[0]:
|
if callback_type == 'like' and res[0]:
|
||||||
await self._update_menu_views_dict(interaction)
|
await self._update_menu_views_dict(interaction)
|
||||||
await interaction.edit(view=menu_views[gid])
|
await interaction.edit(view=menu_views[gid])
|
||||||
|
await interaction.respond(
|
||||||
|
f"✅ Трек был {'добавлен в понравившиеся.' if res[1] == 'added' else 'удалён из понравившихся.'}",
|
||||||
|
delete_after=15, ephemeral=True
|
||||||
|
)
|
||||||
|
|
||||||
elif callback_type == 'dislike' and res[0]:
|
elif callback_type == 'dislike' and res[0]:
|
||||||
await self.next_track(interaction, vc=vc, button_callback=True)
|
|
||||||
|
if len(channel.members) == 2 and not await self.next_track(interaction, vc=vc, button_callback=True):
|
||||||
|
await interaction.respond("✅ Воспроизведение приостановлено. Нет треков в очереди.", delete_after=15)
|
||||||
|
|
||||||
|
await self._update_menu_views_dict(interaction)
|
||||||
|
await interaction.edit(view=menu_views[gid])
|
||||||
|
await interaction.respond(
|
||||||
|
f"✅ Трек был {'добавлен в дизлайки.' if res[1] == 'added' else 'удалён из дизлайков.'}",
|
||||||
|
delete_after=15, ephemeral=True
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logging.debug(f"[VC_EXT] Failed to {callback_type} track")
|
logging.debug(f"[VC_EXT] Failed to get {callback_type} tracks")
|
||||||
await interaction.respond("❌ Операция не удалась. Попробуйте позже.")
|
await interaction.respond("❌ Операция не удалась. Попробуйте позже.", delete_after=15, ephemeral=True)
|
||||||
|
|
||||||
class LyricsButton(Button, VoiceExtension):
|
class LyricsButton(Button, VoiceExtension):
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
@@ -192,7 +303,7 @@ class MyVibeButton(Button, VoiceExtension):
|
|||||||
if next_track:
|
if next_track:
|
||||||
# Need to avoid additional feedback.
|
# Need to avoid additional feedback.
|
||||||
# TODO: Make it more elegant
|
# TODO: Make it more elegant
|
||||||
await self._play_next_track(interaction, next_track, button_callback=True)
|
await self._play_track(interaction, next_track, button_callback=True)
|
||||||
|
|
||||||
class MyVibeSelect(Select, VoiceExtension):
|
class MyVibeSelect(Select, VoiceExtension):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -214,23 +325,23 @@ class MyVibeSelect(Select, VoiceExtension):
|
|||||||
logging.warning(f'[MENU] Unknown custom_id: {custom_id}')
|
logging.warning(f'[MENU] Unknown custom_id: {custom_id}')
|
||||||
return
|
return
|
||||||
|
|
||||||
if not interaction.data or 'values' not in interaction.data:
|
if not interaction.data:
|
||||||
logging.warning('[MENU] No data in select callback')
|
logging.warning('[MENU] No data in select callback')
|
||||||
return
|
return
|
||||||
|
|
||||||
data_value = interaction.data['values'][0]
|
data_values = cast(list[str] | None, interaction.data.get('values'))
|
||||||
if data_value not in (
|
if not data_values or data_values[0] not in (
|
||||||
'fun', 'active', 'calm', 'sad', 'all',
|
'fun', 'active', 'calm', 'sad', 'all',
|
||||||
'favorite', 'popular', 'discover', 'default',
|
'favorite', 'popular', 'discover', 'default',
|
||||||
'not-russian', 'russian', 'without-words', 'any'
|
'not-russian', 'russian', 'without-words', 'any'
|
||||||
):
|
):
|
||||||
logging.warning(f'[MENU] Unknown data_value: {data_value}')
|
logging.warning(f'[MENU] Unknown data_value: {data_values}')
|
||||||
return
|
return
|
||||||
|
|
||||||
logging.info(f"[MENU] Settings option '{custom_id}' updated to {data_value}")
|
logging.info(f"[MENU] Settings option '{custom_id}' updated to '{data_values[0]}'")
|
||||||
await self.users_db.update(interaction.user.id, {f'vibe_settings.{custom_id}': data_value})
|
await self.users_db.update(interaction.user.id, {f'vibe_settings.{custom_id}': data_values[0]})
|
||||||
|
|
||||||
view = MyVibeSettingsView(interaction)
|
view = await MyVibeSettingsView(interaction).init()
|
||||||
view.disable_all_items()
|
view.disable_all_items()
|
||||||
await interaction.edit(view=view)
|
await interaction.edit(view=view)
|
||||||
|
|
||||||
@@ -330,10 +441,15 @@ class AddToPlaylistSelect(Select, VoiceExtension):
|
|||||||
logging.warning('[MENU] No data in select callback')
|
logging.warning('[MENU] No data in select callback')
|
||||||
return
|
return
|
||||||
|
|
||||||
data = interaction.data['values'][0].split(';')
|
data_values = cast(list[str] | None, interaction.data.get('values'))
|
||||||
logging.debug(f"[MENU] Add to playlist select callback: {data}")
|
logging.debug(f"[MENU] Add to playlist select callback: {data_values}")
|
||||||
|
|
||||||
playlist = cast(Playlist, await self.ym_client.users_playlists(kind=data[0], user_id=data[1]))
|
if not data_values:
|
||||||
|
logging.warning('[MENU] No data in select callback')
|
||||||
|
return
|
||||||
|
|
||||||
|
kind, user_id = data_values[0].split(';')
|
||||||
|
playlist = cast(Playlist, await self.ym_client.users_playlists(kind=kind, user_id=user_id))
|
||||||
current_track = await self.db.get_track(interaction.guild_id, 'current')
|
current_track = await self.db.get_track(interaction.guild_id, 'current')
|
||||||
|
|
||||||
if not current_track:
|
if not current_track:
|
||||||
@@ -362,14 +478,19 @@ class AddToPlaylistButton(Button, VoiceExtension):
|
|||||||
return
|
return
|
||||||
|
|
||||||
client = await self.init_ym_client(interaction)
|
client = await self.init_ym_client(interaction)
|
||||||
if not client or not client.me or not client.me.account or not client.me.account.uid:
|
if not client:
|
||||||
await interaction.respond('❌ Что-то пошло не так. Попробуйте позже.', ephemeral=True)
|
await interaction.respond('❌ Что-то пошло не так. Попробуйте позже.', delete_after=15, ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not (vc := await self.get_voice_client(interaction)) or not vc.is_playing:
|
if not (vc := await self.get_voice_client(interaction)) or not vc.is_playing:
|
||||||
await interaction.respond("❌ Нет воспроизводимого трека.", delete_after=15, ephemeral=True)
|
await interaction.respond("❌ Нет воспроизводимого трека.", delete_after=15, ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
playlists = await client.users_playlists_list()
|
||||||
|
if not playlists:
|
||||||
|
await interaction.respond('❌ У вас нет плейлистов.', delete_after=15, ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
view = View(
|
view = View(
|
||||||
AddToPlaylistSelect(
|
AddToPlaylistSelect(
|
||||||
client,
|
client,
|
||||||
@@ -379,7 +500,7 @@ class AddToPlaylistButton(Button, VoiceExtension):
|
|||||||
SelectOption(
|
SelectOption(
|
||||||
label=playlist.title or "Без названия",
|
label=playlist.title or "Без названия",
|
||||||
value=f"{playlist.kind or "-1"};{playlist.uid}"
|
value=f"{playlist.kind or "-1"};{playlist.uid}"
|
||||||
) for playlist in await client.users_playlists_list(client.me.account.uid)
|
) for playlist in playlists
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -427,10 +548,9 @@ class MenuView(View, VoiceExtension):
|
|||||||
self.add_item(self.next_button)
|
self.add_item(self.next_button)
|
||||||
self.add_item(self.shuffle_button)
|
self.add_item(self.shuffle_button)
|
||||||
|
|
||||||
if not isinstance(self.ctx, RawReactionActionEvent) and len(cast(VoiceChannel, self.ctx.channel).members) > 2:
|
if not isinstance(self.ctx, RawReactionActionEvent) and len(cast(VoiceChannel, self.ctx.channel).members) == 2:
|
||||||
self.dislike_button.disabled = True
|
if likes and current_track and str(current_track['id']) in [str(like.id) for like in likes]:
|
||||||
elif likes and current_track and str(current_track['id']) in [str(like.id) for like in likes]:
|
self.like_button.style = ButtonStyle.success
|
||||||
self.like_button.style = ButtonStyle.success
|
|
||||||
|
|
||||||
if not current_track:
|
if not current_track:
|
||||||
self.lyrics_button.disabled = True
|
self.lyrics_button.disabled = True
|
||||||
@@ -444,7 +564,7 @@ class MenuView(View, VoiceExtension):
|
|||||||
self.add_item(self.dislike_button)
|
self.add_item(self.dislike_button)
|
||||||
self.add_item(self.lyrics_button)
|
self.add_item(self.lyrics_button)
|
||||||
self.add_item(self.add_to_playlist_button)
|
self.add_item(self.add_to_playlist_button)
|
||||||
|
|
||||||
if self.guild['vibing']:
|
if self.guild['vibing']:
|
||||||
self.add_item(self.vibe_settings_button)
|
self.add_item(self.vibe_settings_button)
|
||||||
else:
|
else:
|
||||||
@@ -469,3 +589,4 @@ class MenuView(View, VoiceExtension):
|
|||||||
logging.debug('[MENU] Successfully deleted menu message')
|
logging.debug('[MENU] Successfully deleted menu message')
|
||||||
else:
|
else:
|
||||||
logging.debug('[MENU] No menu message found')
|
logging.debug('[MENU] No menu message found')
|
||||||
|
self.stop()
|
||||||
|
|||||||
Reference in New Issue
Block a user