Merge pull request #2 from Lemon4ksan/dev

Обновление бота #2
This commit is contained in:
Bananchiki
2025-02-21 19:44:58 +03:00
committed by GitHub
9 changed files with 711 additions and 685 deletions

View File

@@ -1,19 +1,16 @@
import logging
from typing import Literal, cast
from typing import Literal
from asyncio import gather
import discord
from discord.ext.commands import Cog
import yandex_music
import yandex_music.exceptions
from yandex_music.exceptions import UnauthorizedError
from yandex_music import ClientAsync as YMClient
from yandex_music import Track, Album, Artist, Playlist
from MusicBot.database import BaseUsersDatabase, BaseGuildsDatabase
from MusicBot.ui import ListenView
from MusicBot.cogs.utils.embeds import generate_item_embed
from MusicBot.database import BaseUsersDatabase, BaseGuildsDatabase
from MusicBot.cogs.utils import generate_item_embed
users_db = BaseUsersDatabase()
@@ -24,41 +21,37 @@ async def get_search_suggestions(ctx: discord.AutocompleteContext) -> list[str]:
if not ctx.interaction.user or not ctx.value or len(ctx.value) < 2:
return []
token = await users_db.get_ym_token(ctx.interaction.user.id)
uid = ctx.interaction.user.id
token = await users_db.get_ym_token(uid)
if not token:
logging.info(f"[GENERAL] User {ctx.interaction.user.id} has no token")
logging.info(f"[GENERAL] User {uid} has no token")
return []
try:
client = await YMClient(token).init()
except yandex_music.exceptions.UnauthorizedError:
logging.info(f"[GENERAL] User {ctx.interaction.user.id} provided invalid token")
except UnauthorizedError:
logging.info(f"[GENERAL] User {uid} provided invalid token")
return []
content_type = ctx.options['тип']
search = await client.search(ctx.value)
if not search:
logging.warning(f"[GENERAL] Failed to search for '{ctx.value}' for user {ctx.interaction.user.id}")
logging.warning(f"[GENERAL] Failed to search for '{ctx.value}' for user {uid}")
return []
res = []
logging.debug(f"[GENERAL] Searching for '{ctx.value}' for user {ctx.interaction.user.id}")
logging.debug(f"[GENERAL] Searching for '{ctx.value}' for user {uid}")
if content_type == 'Трек' and search.tracks:
for item in search.tracks.results:
res.append(f"{item.title} {f"({item.version})" if item.version else ''} - {", ".join(item.artists_name())}")
elif content_type == 'Альбом' and search.albums:
for item in search.albums.results:
res.append(f"{item.title} - {", ".join(item.artists_name())}")
elif content_type == 'Артист' and search.artists:
for item in search.artists.results:
res.append(f"{item.name}")
elif content_type == 'Плейлист' and search.playlists:
for item in search.playlists.results:
res.append(f"{item.title}")
elif content_type == "Свой плейлист":
playlists_list = await client.users_playlists_list()
res = [playlist.title if playlist.title else 'Без названия' for playlist in playlists_list]
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]
elif content_type == 'Альбом' and search.albums is not None:
res = [f"{item.title} - {", ".join(item.artists_name())}" for item in search.albums.results]
elif content_type == 'Артист' and search.artists is not None:
res = [f"{item.name}" for item in search.artists.results]
elif content_type == 'Плейлист' and search.playlists is not None:
res = [f"{item.title}" for item in search.playlists.results]
else:
logging.warning(f"[GENERAL] Invalid content type '{content_type}' for user {uid}")
return []
return res[:100]
@@ -66,17 +59,20 @@ async def get_user_playlists_suggestions(ctx: discord.AutocompleteContext) -> li
if not ctx.interaction.user or not ctx.value or len(ctx.value) < 2:
return []
token = await users_db.get_ym_token(ctx.interaction.user.id)
uid = ctx.interaction.user.id
token = await users_db.get_ym_token(uid)
if not token:
logging.info(f"[GENERAL] User {ctx.interaction.user.id} has no token")
logging.info(f"[GENERAL] User {uid} has no token")
return []
try:
client = await YMClient(token).init()
except yandex_music.exceptions.UnauthorizedError:
logging.info(f"[GENERAL] User {ctx.interaction.user.id} provided invalid token")
except UnauthorizedError:
logging.info(f"[GENERAL] User {uid} provided invalid token")
return []
logging.debug(f"[GENERAL] Searching for '{ctx.value}' for user {uid}")
playlists_list = await client.users_playlists_list()
return [playlist.title for playlist in playlists_list if playlist.title and ctx.value in playlist.title][:100]
@@ -94,12 +90,11 @@ class General(Cog):
"command",
description="Название команды.",
type=discord.SlashCommandOptionType.string,
default='all'
required=False
)
async def help(self, ctx: discord.ApplicationContext, command: str) -> None:
async def help(self, ctx: discord.ApplicationContext, command: str = 'all') -> None:
logging.info(f"[GENERAL] Help command invoked by {ctx.user.id} for command '{command}'")
response_message = None
embed = discord.Embed(
title='Помощь',
color=0xfed42b
@@ -113,10 +108,8 @@ class General(Cog):
"Зарегистрируйте свой токен с помощью /login. Его можно получить [здесь](https://github.com/MarshalX/yandex-music-api/discussions/513).\n"
"Для получения помощи по конкретной команде, введите /help <команда>.\n"
"Для изменения настроек необходимо иметь права управления каналами на сервере.\n\n"
"Помните, что это **не замена Яндекс Музыки**, а лишь её дополнение. Не ожидайте безупречного звука.\n\n"
"**Для дополнительной помощи, присоединяйтесь к [серверу любителей Яндекс Музыки](https://discord.gg/gkmFDaPMeC).**"
"**Присоединяйтесь к нашему [серверу сообщества](https://discord.gg/TgnW8nfbFn)!**"
)
embed.add_field(
name='__Основные команды__',
value="""`account`
@@ -124,10 +117,8 @@ class General(Cog):
`help`
`queue`
`settings`
`track`
`voice`"""
)
embed.set_footer(text='©️ Bananchiki')
elif command == 'account':
embed.description += (
@@ -145,8 +136,7 @@ class General(Cog):
)
elif command == 'help':
embed.description += (
"Вывести список всех команд.\n```/help```\n"
"Получить информацию о конкретной команде.\n```/help <команда>```"
"Вывести список всех команд или информацию по конкретной команде.\n```/help <команда>```\n"
)
elif command == 'queue':
embed.description += (
@@ -158,34 +148,22 @@ class General(Cog):
embed.description += (
"`Примечание`: Только пользователи с разрешением управления каналом могут менять настройки.\n\n"
"Получить текущие настройки.\n```/settings show```\n"
"Разрешить или запретить воспроизведение Explicit треков и альбомов. Если автор или плейлист содержат Explicit треки, убираются кнопки для доступа к ним.\n```/settings explicit```\n"
"Разрешить или запретить создание меню проигрывателя, когда в канале больше одного человека.\n```/settings menu```\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```"
"Переключить параметр настроек.\n```/settings toggle <параметр>```\n"
)
elif command == 'voice':
embed.description += (
"`Примечание`: Доступность меню и Моей Волны зависит от настроек сервера.\n\n"
"Присоединить бота в голосовой канал.\n```/voice join```\n"
"Заставить бота покинуть голосовой канал.\n ```/voice leave```\n"
"Прервать проигрывание, удалить историю, очередь и текущий плеер.\n ```/voice stop```\n"
"Создать меню проигрывателя. \n```/voice menu```\n"
"Запустить станцию. Без уточнения станции, запускает Мою Волну.\n```/voice vibe <название станции>```"
)
else:
response_message = '❌ Неизвестная команда.'
embed = None
await ctx.respond('❌ Неизвестная команда.', delete_after=15, ephemeral=True)
return
await ctx.respond(response_message, embed=embed, ephemeral=True)
await ctx.respond(embed=embed, ephemeral=True)
@account.command(description="Ввести токен Яндекс Музыки.")
@discord.option("token", type=discord.SlashCommandOptionType.string, description="Токен.")
@@ -193,16 +171,20 @@ class General(Cog):
logging.info(f"[GENERAL] Login command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
try:
client = await YMClient(token).init()
except yandex_music.exceptions.UnauthorizedError:
except UnauthorizedError:
logging.info(f"[GENERAL] Invalid token provided by user {ctx.author.id}")
await ctx.respond('❌ Недействительный токен.', delete_after=15, ephemeral=True)
return
about = cast(yandex_music.Status, client.me).to_dict()
uid = ctx.author.id
await self.users_db.update(uid, {'ym_token': token})
logging.info(f"[GENERAL] Token saved for user {ctx.author.id}")
await ctx.respond(f'Привет, {about['account']['first_name']}!', delete_after=15, ephemeral=True)
if not client.me or not client.me.account:
logging.warning(f"[GENERAL] Failed to get user info for user {ctx.author.id}")
await ctx.respond('Не удалось получить информацию о пользователе.', delete_after=15, ephemeral=True)
return
await self.users_db.update(ctx.author.id, {'ym_token': token})
await ctx.respond(f'✅ Привет, {client.me.account.first_name}!', delete_after=15, ephemeral=True)
logging.info(f"[GENERAL] User {ctx.author.id} logged in successfully")
@account.command(description="Удалить токен из базы данных бота.")
async def remove(self, ctx: discord.ApplicationContext) -> None:
@@ -213,7 +195,8 @@ class General(Cog):
return
await self.users_db.update(ctx.user.id, {'ym_token': None})
await ctx.respond(f'Токен был удалён.', delete_after=15, ephemeral=True)
await ctx.respond(f'Токен был удалён.', delete_after=15, ephemeral=True)
logging.info(f"[GENERAL] Token removed for user {ctx.author.id}")
@account.command(description="Получить плейлист «Мне нравится»")
async def likes(self, ctx: discord.ApplicationContext) -> None:
@@ -241,11 +224,12 @@ class General(Cog):
await ctx.respond('У вас нет треков в плейлисте «Мне нравится».', delete_after=15, ephemeral=True)
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)
tracks = [track for track in real_tracks if not isinstance(track, BaseException)] # Can't fetch user tracks
embed = await generate_item_embed(tracks)
logging.info(f"[GENERAL] Successfully fetched likes for user {ctx.user.id}")
await ctx.respond(embed=embed, view=ListenView(tracks))
await ctx.respond(embed=await generate_item_embed(tracks), view=ListenView(tracks))
logging.info(f"[GENERAL] Successfully generated likes message for user {ctx.user.id}")
@account.command(description="Получить ваши рекомендации.")
@discord.option(
@@ -263,15 +247,19 @@ class General(Cog):
# NOTE: Recommendations can be accessed by using /find, but it's more convenient to have it in separate command.
logging.debug(f"[GENERAL] Recommendations command invoked by user {ctx.user.id} in guild {ctx.guild_id} for type '{content_type}'")
guild = await self.db.get_guild(ctx.guild_id)
token = await self.users_db.get_ym_token(ctx.user.id)
if not token:
await ctx.respond("❌ Укажите токен через /account login.", delete_after=15, ephemeral=True)
return
client = await YMClient(token).init()
try:
client = await YMClient(token).init()
except UnauthorizedError:
logging.info(f"[GENERAL] User {ctx.user.id} provided invalid token")
await ctx.respond("❌ Недействительный токен. Если это не так, попробуйте ещё раз.", delete_after=15, ephemeral=True)
return
search = await client.search(content_type, False, 'playlist')
search = await client.search(content_type, type_='playlist')
if not search or not search.playlists:
logging.info(f"[GENERAL] Failed to fetch recommendations for user {ctx.user.id}")
await ctx.respond('❌ Что-то пошло не так. Повторите попытку позже.', delete_after=15, ephemeral=True)
@@ -288,18 +276,7 @@ class General(Cog):
await ctx.respond("❌ Пустой плейлист.", delete_after=15, ephemeral=True)
return
embed = await generate_item_embed(playlist)
view = ListenView(playlist)
for track_short in playlist.tracks:
track = cast(Track, track_short.track)
if (track.explicit or track.content_warning) and not guild['allow_explicit']:
logging.info(f"[GENERAL] User {ctx.user.id} search for '{content_type}' returned explicit content and is not allowed on this server")
embed.set_footer(text="Воспроизведение недоступно, так как в плейлисте присутствуют Explicit треки")
view = None
break
await ctx.respond(embed=embed, view=view)
await ctx.respond(embed=await generate_item_embed(playlist), view=ListenView(playlist))
@account.command(description="Получить ваш плейлист.")
@discord.option(
@@ -312,7 +289,6 @@ class General(Cog):
async def playlist(self, ctx: discord.ApplicationContext, name: str) -> None:
logging.info(f"[GENERAL] Playlists command invoked by user {ctx.user.id} in guild {ctx.guild_id}")
guild = await self.db.get_guild(ctx.guild_id, projection={'allow_explicit': 1})
token = await self.users_db.get_ym_token(ctx.user.id)
if not token:
logging.info(f"[GENERAL] No token found for user {ctx.user.id}")
@@ -321,7 +297,7 @@ class General(Cog):
try:
client = await YMClient(token).init()
except yandex_music.exceptions.UnauthorizedError:
except UnauthorizedError:
logging.info(f"[GENERAL] User {ctx.user.id} provided invalid token")
await ctx.respond("❌ Недействительный токен. Если это не так, попробуйте ещё раз.", delete_after=15, ephemeral=True)
return
@@ -340,18 +316,7 @@ class General(Cog):
await ctx.respond("❌ Плейлист пуст.", delete_after=15, ephemeral=True)
return
embed = await generate_item_embed(playlist)
view = ListenView(playlist)
for track_short in playlist.tracks:
track = cast(Track, track_short.track)
if (track.explicit or track.content_warning) and not guild['allow_explicit']:
logging.info(f"[GENERAL] User {ctx.user.id} search for '{name}' returned explicit content and is not allowed on this server")
embed.set_footer(text="Воспроизведение недоступно, так как в плейлисте присутствуют Explicit треки")
view = None
break
await ctx.respond(embed=embed, view=view)
await ctx.respond(embed=await generate_item_embed(playlist), view=ListenView(playlist))
@discord.slash_command(description="Найти контент и отправить информацию о нём. Возвращается лучшее совпадение.")
@discord.option(
@@ -374,11 +339,8 @@ class General(Cog):
content_type: Literal['Трек', 'Альбом', 'Артист', 'Плейлист'],
name: str
) -> None:
# TODO: Improve explicit check by excluding bad tracks from the queue and not fully discard the artist/album/playlist.
logging.info(f"[GENERAL] Find command invoked by user {ctx.user.id} in guild {ctx.guild_id} for '{content_type}' with name '{name}'")
guild = await self.db.get_guild(ctx.guild_id, projection={'allow_explicit': 1})
token = await self.users_db.get_ym_token(ctx.user.id)
if not token:
logging.info(f"[GENERAL] No token found for user {ctx.user.id}")
@@ -387,66 +349,32 @@ class General(Cog):
try:
client = await YMClient(token).init()
except yandex_music.exceptions.UnauthorizedError:
except UnauthorizedError:
logging.info(f"[GENERAL] User {ctx.user.id} provided invalid token")
await ctx.respond("❌ Недействительный токен. Если это не так, попробуйте ещё раз.", delete_after=15, ephemeral=True)
return
result = await client.search(name, nocorrect=True)
if not result:
search_result = await client.search(name, nocorrect=True)
if not search_result:
logging.warning(f"Failed to search for '{name}' for user {ctx.user.id}")
await ctx.respond("❌ Что-то пошло не так. Повторите попытку позже.", delete_after=15, ephemeral=True)
return
if content_type == 'Трек':
content = result.tracks
content = search_result.tracks
elif content_type == 'Альбом':
content = result.albums
content = search_result.albums
elif content_type == 'Артист':
content = result.artists
elif content_type == 'Плейлист':
content = result.playlists
content = search_result.artists
else:
content = search_result.playlists
if not content:
logging.info(f"[GENERAL] User {ctx.user.id} search for '{name}' returned no results")
await ctx.respond("❌ По запросу ничего не найдено.", delete_after=15, ephemeral=True)
return
content = content.results[0]
embed = await generate_item_embed(content)
view = ListenView(content)
if isinstance(content, (Track, Album)) and (content.explicit or content.content_warning) and not guild['allow_explicit']:
logging.info(f"[GENERAL] User {ctx.user.id} search for '{name}' returned explicit content and is not allowed on this server")
await ctx.respond("❌ Explicit контент запрещён на этом сервере.", delete_after=15, ephemeral=True)
return
elif isinstance(content, Artist):
tracks = await content.get_tracks_async()
if not tracks:
logging.info(f"[GENERAL] User {ctx.user.id} search for '{name}' returned no tracks")
await ctx.respond("❌ Треки от этого исполнителя не найдены.", delete_after=15, ephemeral=True)
return
for track in tracks:
if (track.explicit or track.content_warning) and not guild['allow_explicit']:
logging.info(f"[GENERAL] User {ctx.user.id} search for '{name}' returned explicit content and is not allowed on this server")
view = None
embed.set_footer(text="Воспроизведение недоступно, так как у автора присутствуют Explicit треки")
break
elif isinstance(content, Playlist):
tracks = await content.fetch_tracks_async()
if not tracks:
logging.info(f"[GENERAL] User {ctx.user.id} search for '{name}' returned no tracks")
await ctx.respond("❌ Пустой плейлист.", delete_after=15, ephemeral=True)
return
for track_short in content.tracks:
track = cast(Track, track_short.track)
if (track.explicit or track.content_warning) and not guild['allow_explicit']:
logging.info(f"[GENERAL] User {ctx.user.id} search for '{name}' returned explicit content and is not allowed on this server")
view = None
embed.set_footer(text="Воспроизведение недоступно, так как в плейлисте присутствуют Explicit треки")
break
result = content.results[0]
await ctx.respond(embed=await generate_item_embed(result), view=ListenView(result))
logging.info(f"[GENERAL] Successfully generated '{content_type}' message for user {ctx.author.id}")
await ctx.respond(embed=embed, view=view)

View File

@@ -19,114 +19,53 @@ class Settings(Cog):
@settings.command(name="show", description="Показать текущие настройки бота.")
async def show(self, ctx: discord.ApplicationContext) -> None:
guild = await self.db.get_guild(ctx.guild.id, projection={
'allow_explicit': 1, 'always_allow_menu': 1,
'vote_next_track': 1, 'vote_add_track': 1, 'vote_add_album': 1, 'vote_add_artist': 1, 'vote_add_playlist': 1,
'allow_connect': 1, 'allow_disconnect': 1
})
guild = await self.db.get_guild(ctx.guild.id, projection={'allow_change_connect': 1, 'vote_switch_track': 1, 'vote_add': 1})
vote = "✅ - Переключение" if guild['vote_switch_track'] else "❌ - Переключение"
vote += "\n✅ - Добавление в очередь" if guild['vote_add'] else "\n❌ - Добавление в очередь"
connect = "\n✅ - Разрешено всем" if guild['allow_change_connect'] else "\n❌ - Только для участникам с правами управления каналом"
embed = discord.Embed(title="Настройки бота", color=0xfed42b)
explicit = "✅ - Разрешены" if guild['allow_explicit'] else "❌ - Запрещены"
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="__Explicit треки__", value=explicit, inline=False)
embed.add_field(name="__Меню проигрывателя__", value=menu, 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)
@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})
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="explicit", description="Разрешить или запретить воспроизведение Explicit треков (пока что неполноценно).")
async def explicit(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_explicit': 1})
await self.db.update(ctx.guild.id, {'allow_explicit': not guild['allow_explicit']})
await ctx.respond(f"Треки с содержанием не для детей теперь {'✅ разрешены' if not guild['allow_explicit'] 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="Настроить голосование.")
@settings.command(name="toggle", description="Переключить параметр настроек.")
@discord.option(
"vote_type",
"параметр",
parameter_name="vote_type",
description="Тип голосования.",
type=discord.SlashCommandOptionType.string,
choices=['+Всё', '-Всё', 'Переключение', 'Трек', 'Альбом', 'Плейлист'],
default='+Всё'
choices=['Переключение', 'Добавление в очередь', 'Добавление/Отключение бота']
)
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)
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={'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, {
'vote_next_track': False,
'vote_add_track': False,
'vote_add_album': False,
'vote_add_artist': False,
'vote_add_playlist': False
}
)
response_message = "Голосование ❌ выключено."
elif vote_type == '+Всё':
await self.db.update(ctx.guild.id, {
'vote_next_track': True,
'vote_add_track': True,
'vote_add_album': True,
'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 "✅ включено.")
if vote_type == 'Переключение':
await self.db.update(ctx.guild.id, {'vote_switch_track': not guild['vote_switch_track']})
response_message = "Голосование за переключение трека " + ("❌ выключено." if guild['vote_switch_track'] else "✅ включено.")
elif vote_type == 'Добавление в очередь':
await self.db.update(ctx.guild.id, {'vote_add': not guild['vote_add']})
response_message = "Голосование за добавление в очередь " + ("❌ выключено." if guild['vote_add'] else "✅ включено.")
elif vote_type == 'Добавление/Отключение бота':
await self.db.update(ctx.guild.id, {'allow_change_connect': not guild['allow_change_connect']})
response_message = f"Добавление/Отключение бота от канала теперь {'✅ разрешено' if not guild['allow_change_connect'] else '❌ запрещено'} участникам без прав управления каналом."
else:
response_message = "❌ Неизвестный тип голосования."
await ctx.respond(response_message, delete_after=15, ephemeral=True)

View File

@@ -9,10 +9,10 @@ from yandex_music import Track, TrackShort, ClientAsync as YMClient
import discord
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.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.
@@ -23,13 +23,16 @@ class VoiceExtension:
self.db = VoiceGuildsDatabase()
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.
Args:
ctx (ApplicationContext | Interaction): Context.
ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context.
disable (bool, optional): Disable menu message. Defaults to False.
Raises:
ValueError: If bot instance is not set and ctx is RawReactionActionEvent.
Returns:
bool: True if sent, False if not.
"""
@@ -65,7 +68,23 @@ class VoiceExtension:
await message.delete()
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
await self.db.update(ctx.guild_id, {'current_menu': response.id})
@@ -77,7 +96,7 @@ class VoiceExtension:
Reset `current_menu` field in the database if not found.
Args:
ctx (ApplicationContext | Interaction): Context.
ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context.
menu_mid (int): Id of the menu message to fetch.
Returns:
@@ -105,18 +124,17 @@ class VoiceExtension:
await self.db.update(ctx.guild_id, {'current_menu': None})
return None
if menu:
logging.debug(f"[VC_EXT] Menu message {menu_mid} successfully fetched")
else:
if not menu:
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})
return None
logging.debug(f"[VC_EXT] Menu message {menu_mid} successfully fetched")
return menu
async def update_menu_full(
self,
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
menu_mid: int | None = None,
*,
menu_message: discord.Message | None = None,
button_callback: bool = False
@@ -124,7 +142,7 @@ class VoiceExtension:
"""Update embed and view of the current menu message. Return True if updated.
Args:
ctx (ApplicationContext | Interaction): Context.
ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context.
menu_mid (int): Id of the menu message to update. Defaults to None.
menu_message (discord.Message | None): Message to update. If None, fetches menu from channel using `menu_mid`. Defaults to None.
button_callback (bool, optional): Should be True if the function is being called from button callback. Defaults to False.
@@ -132,7 +150,7 @@ class VoiceExtension:
Returns:
bool: True if updated, False if not.
"""
logging.debug(
logging.info(
f"[VC_EXT] Updating menu embed using " + (
"interaction context" if isinstance(ctx, Interaction) 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'")
return False
guild = await self.db.get_guild(gid, projection={'vibing': 1, 'current_track': 1})
if not menu_message:
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)
guild = await self.db.get_guild(gid, projection={'vibing': 1, 'current_menu': 1, 'current_track': 1})
if not guild['current_menu']:
return False
menu_message = await self.get_menu_message(ctx, guild['current_menu']) if not menu_message else menu_message
if not menu_message:
return False
@@ -168,6 +183,16 @@ class VoiceExtension:
))
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)
try:
if isinstance(ctx, Interaction) and button_callback:
@@ -186,7 +211,6 @@ class VoiceExtension:
async def update_menu_view(
self,
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
guild: ExplicitGuild,
*,
menu_message: discord.Message | None = None,
button_callback: bool = False,
@@ -206,6 +230,11 @@ class VoiceExtension:
"""
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']:
return False
@@ -217,10 +246,10 @@ class VoiceExtension:
try:
if isinstance(ctx, Interaction) and button_callback:
# If interaction from menu buttons
await ctx.edit(view=menu_views[guild['_id']])
await ctx.edit(view=menu_views[ctx.guild_id])
else:
# 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:
logging.warning("[VC_EXT] Menu message not found")
return False
@@ -230,19 +259,21 @@ class VoiceExtension:
async def update_vibe(
self,
ctx: ApplicationContext | Interaction,
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
type: str,
id: str | int,
*,
viber_id: int | None = None,
update_settings: bool = False
) -> bool:
"""Update vibe state or initialize it if not `guild['vibing']` and replace queue with next tracks.
User's vibe has type `user` and id `onyourwave`.
Args:
ctx (ApplicationContext | Interaction): Context.
ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context.
type (str): Type of the item.
id (str | int): ID of the item.
viber_id (int | None, optional): ID of the user who started vibe. If None, uses user id in context. Defaults to None.
update_settings (bool, optional): Update vibe settings by sending feedack usind data from database. Defaults to False.
Returns:
@@ -251,7 +282,7 @@ class VoiceExtension:
logging.info(f"[VC_EXT] Updating vibe for guild {ctx.guild_id} with type '{type}' and id '{id}'")
gid = ctx.guild_id
uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None
uid = viber_id if viber_id else ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None
if not uid or not gid:
logging.warning("[VC_EXT] Guild ID or User ID not found in context inside 'vibe_update'")
@@ -340,7 +371,7 @@ class VoiceExtension:
if not isinstance(ctx.channel, discord.VoiceChannel):
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
if ctx.user.id not in ctx.channel.voice_states:
@@ -356,8 +387,7 @@ class VoiceExtension:
if check_vibe_privilage:
guild = await self.db.get_guild(ctx.guild.id, projection={'current_viber_id': 1, 'vibing': 1})
member = cast(discord.Member, ctx.user)
if guild['vibing'] and ctx.user.id != guild['current_viber_id'] and not member.guild_permissions.manage_channels:
if guild['vibing'] and ctx.user.id != guild['current_viber_id']:
logging.debug("[VIBE] Context user is not the current viber")
await ctx.respond("❌ Вы не можете взаимодействовать с чужой волной!", delete_after=15, ephemeral=True)
return False
@@ -369,7 +399,7 @@ class VoiceExtension:
"""Return voice client for the given guild id. Return None if not present.
Args:
ctx (ApplicationContext | Interaction): Command context.
ctx (ApplicationContext | Interaction | RawReactionActionEvent): Command context.
Returns:
(discord.VoiceClient | None): Voice client or None.
@@ -408,10 +438,10 @@ class VoiceExtension:
retry: bool = False
) -> str | None:
"""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:
ctx (ApplicationContext | Interaction): Context.
ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context.
track (Track): Track to play.
vc (discord.VoiceClient | None): Voice client.
menu_message (discord.Message | None): Menu message. If None, fetches menu from channel using message id from database. Defaults to None.
@@ -442,10 +472,13 @@ class VoiceExtension:
if not isinstance(ctx, RawReactionActionEvent) and ctx.channel:
channel = cast(discord.VoiceChannel, ctx.channel)
elif self.bot and isinstance(ctx, RawReactionActionEvent):
channel = cast(discord.VoiceChannel, self.bot.get_channel(ctx.channel_id))
if not retry:
return await self.play_track(ctx, track, vc=vc, button_callback=button_callback, retry=True)
logging.error(f"[VC_EXT] Failed to download track '{track.title}'")
await channel.send(f"😔 Не удалось загрузить трек. Попробуйте сбросить меню.", delete_after=15)
return None
@@ -457,25 +490,35 @@ class VoiceExtension:
if menu_message or guild['current_menu']:
# 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']:
# Giving FFMPEG enough time to process the audio file
await asyncio.sleep(1)
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)
elif self.bot:
channel = cast(discord.VoiceChannel, self.bot.get_channel(ctx.channel_id))
await channel.send(f"Не удалось проиграть трек. Попробуйте сбросить меню.", delete_after=15)
return None
logging.info(f"[VC_EXT] Playing track '{track.title}'")
await self.db.update(gid, {'is_stopped': False})
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
async def stop_playing(
self, ctx: ApplicationContext | Interaction | RawReactionActionEvent,
self,
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
*,
vc: discord.VoiceClient | None = None,
full: bool = False
@@ -514,7 +557,7 @@ class VoiceExtension:
return False
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 True
@@ -532,7 +575,7 @@ class VoiceExtension:
Doesn't change track if stopped. Stop playing if tracks list is empty.
Args:
ctx (ApplicationContext | Interaction): Context
ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context
vc (discord.VoiceClient, optional): Voice client.
after (bool, optional): Whether the function is being called by the after callback. Defaults to False.
menu_message (discord.Message | None): Menu message. If None, fetches menu from channel using message id from database. Defaults to None.
@@ -571,11 +614,16 @@ class VoiceExtension:
await self.db.modify_track(gid, guild['current_track'], 'previous', 'insert')
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']:
if not await self._my_vibe_feedback(ctx, guild, user, client, after=after):
if not isinstance(ctx, RawReactionActionEvent):
await ctx.respond("❌ Что-то пошло не так. Попробуйте снова.", ephemeral=True)
elif self.bot:
channel = cast(discord.VoiceChannel, self.bot.get_channel(ctx.channel_id))
await channel.send("❌ Что-то пошло не так. Попробуйте снова.", delete_after=15)
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):
await ctx.respond("❌ Что-то пошло не так. Попробуйте снова.", ephemeral=True)
return None
if guild['repeat'] and after:
@@ -588,7 +636,7 @@ class VoiceExtension:
logging.debug("[VC_EXT] Getting next track from queue")
next_track = await self.db.get_track(gid, 'next')
if not next_track and guild['vibing'] and not isinstance(ctx, discord.RawReactionActionEvent):
if not next_track and guild['vibing']:
logging.debug("[VC_EXT] No next track found, generating new vibe")
if not user['vibe_type'] or not user['vibe_id']:
logging.warning("[VC_EXT] No vibe type or vibe id found in user data")
@@ -598,7 +646,7 @@ class VoiceExtension:
next_track = await self.db.get_track(gid, 'next')
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 isinstance(ctx, discord.RawReactionActionEvent):
@@ -618,23 +666,28 @@ class VoiceExtension:
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.
Return track title on success.
Args:
ctx (ApplicationContext | Interaction): Context.
ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context.
button_callback (bool, optional): Whether the command was called by a button interaction. Defaults to False.
Returns:
(str | None): Track title or None.
"""
if not ctx.guild or not ctx.user:
logging.warning("Guild or User not found in context inside 'prev_track'")
logging.debug("[VC_EXT] Switching to previous 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
current_track = await self.db.get_track(ctx.guild.id, 'current')
prev_track = await self.db.get_track(ctx.guild.id, 'previous')
current_track = await self.db.get_track(gid, 'current')
prev_track = await self.db.get_track(gid, 'previous')
if prev_track:
logging.debug("[VC_EXT] Previous track found")
@@ -647,7 +700,7 @@ class VoiceExtension:
track = None
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
@@ -655,7 +708,7 @@ class VoiceExtension:
"""Get liked tracks. Return list of tracks on success. Return None if no token found.
Args:
ctx (ApplicationContext | Interaction): Context.
ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context.
Returns:
(list[Track] | None): List of tracks or None.
@@ -721,7 +774,8 @@ class VoiceExtension:
add_func = client.users_dislikes_tracks_add
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)
if str(current_track['id']) not in [str(track.id) for track in tracks]:
@@ -737,7 +791,7 @@ class VoiceExtension:
"""Initialize Yandex Music client. Return client on success. Return None if no token found and respond to the context.
Args:
ctx (ApplicationContext | Interaction): Context.
ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context.
token (str | None, optional): Token. Fetched from database if not provided. Defaults to None.
Returns:
@@ -771,6 +825,113 @@ class VoiceExtension:
self._ym_clients[token] = 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 ctx.guild_id:
logging.warning("[VOICE] Guild not found")
return False
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'):
logging.info(f"[VOICE] No {vote_data['action']} tracks found for message {ctx.message_id}")
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':
if not vote_data['vote_content']:
logging.info(f"[VOICE] Recieved empty vote context for message {ctx.message_id}")
return False
await self.db.modify_track(guild['_id'], vote_data['vote_content'], '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'):
if not vote_data['vote_content']:
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'], vote_data['vote_content'], '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':
if not (vc := await self.get_voice_client(ctx)):
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)
elif vote_data['action'] == 'clear_queue':
await self.db.update(ctx.guild_id, {'previous_tracks': [], 'next_tracks': []})
await channel.send("✅ Очередь и история сброшены.", delete_after=15)
elif vote_data['action'] == 'stop':
res = await self.stop_playing(ctx, full=True)
if res:
await channel.send("✅ Воспроизведение остановлено.", delete_after=15)
else:
await channel.send("❌ Произошла ошибка при остановке воспроизведения.", delete_after=15)
elif vote_data['action'] == 'vibe_station':
_type, _id, viber_id = vote_data['vote_content'] if isinstance(vote_data['vote_content'], list) else (None, None, None)
if not _type or not _id or not viber_id:
logging.warning(f"[VOICE] Recieved empty vote context for message {ctx.message_id}")
await channel.send("❌ Произошла ошибка при обновлении станции.", delete_after=15)
return False
feedback = await self.update_vibe(ctx, _type, _id, viber_id=viber_id)
if not feedback:
await channel.send("❌ Операция не удалась. Возможно, у вес нет подписки на Яндекс Музыку.", delete_after=15)
return False
next_track = await self.db.get_track(ctx.guild_id, 'next')
if next_track:
await self._play_track(ctx, next_track)
else:
logging.error(f"[VOICE] Unknown action '{vote_data['action']}' for message {ctx.message_id}")
return False
return True
async def _update_menu_views_dict(
self,
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
@@ -836,10 +997,11 @@ class VoiceExtension:
})
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.
Args:
ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context.
track (Track): Track.
uid (int): User ID.
@@ -861,7 +1023,7 @@ class VoiceExtension:
logging.debug(f"[VIBE] Track started feedback: {feedback}")
return True
async def _my_vibe_send_stop_feedback(
async def _my_vibe_stop_feedback(
self,
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
guild: ExplicitGuild,
@@ -888,6 +1050,9 @@ class VoiceExtension:
logging.info(f"[VOICE] Failed to init YM client for user {user['_id']}")
if not isinstance(ctx, RawReactionActionEvent):
await ctx.respond("❌ Что-то пошло не так. Попробуйте позже.", delete_after=15, ephemeral=True)
elif self.bot:
channel = cast(discord.VoiceChannel, self.bot.get_channel(ctx.channel_id))
await channel.send("❌ Что-то пошло не так. Попробуйте позже.", delete_after=15)
return False
track = guild['current_track']
@@ -904,9 +1069,9 @@ class VoiceExtension:
logging.info(f"[VOICE] User {user['_id']} finished vibing with result: {res}")
return True
async def _send_next_vibe_feedback(
async def _my_vibe_feedback(
self,
ctx: ApplicationContext | Interaction,
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
guild: ExplicitGuild,
user: ExplicitUser,
client: YMClient,
@@ -917,7 +1082,7 @@ class VoiceExtension:
This is called when a user skips a track or when a track finishes and not when a user stops the player.
Args:
ctx (ApplicationContext | Interaction): Context.
ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context.
guild (ExplicitGuild): Guild.
user (ExplicitUser): User.
client (YMClient): Yandex Music client.
@@ -926,6 +1091,7 @@ class VoiceExtension:
Returns:
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}")
if not user['vibe_type'] or not user['vibe_id']:
@@ -964,21 +1130,21 @@ class VoiceExtension:
return feedback
async def _play_next_track(
async def _play_track(
self,
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
next_track: dict[str, Any],
track: dict[str, Any],
*,
client: YMClient | None = None,
vc: discord.VoiceClient | None = None,
menu_message: discord.Message | None = None,
button_callback: bool = False,
) -> 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:
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.
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.
@@ -986,6 +1152,7 @@ class VoiceExtension:
Returns:
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
if not client:
@@ -998,7 +1165,7 @@ class VoiceExtension:
return None
ym_track = cast(Track, Track.de_json(
next_track,
track,
client=client # type: ignore # Async client can be used here.
))
return await self.play_track(
@@ -1010,7 +1177,7 @@ class VoiceExtension:
)
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:
ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context.

View File

@@ -39,7 +39,6 @@ class Voice(Cog, VoiceExtension):
voice = discord.SlashCommandGroup("voice", "Команды, связанные с голосовым каналом.")
queue = discord.SlashCommandGroup("queue", "Команды, связанные с очередью треков.")
track = discord.SlashCommandGroup("track", "Команды, связанные с треками в голосовом канале.")
def __init__(self, bot: discord.Bot):
VoiceExtension.__init__(self, bot)
@@ -47,21 +46,29 @@ class Voice(Cog, VoiceExtension):
@Cog.listener()
async def on_voice_state_update(self, member: discord.Member, before: discord.VoiceState, after: discord.VoiceState) -> None:
logging.info(f"[VOICE] Voice state update for member {member.id} in guild {member.guild.id}")
gid = member.guild.id
guild = await self.db.get_guild(gid, projection={'current_menu': 1, 'always_allow_menu': 1})
discord_guild = await self.typed_bot.fetch_guild(gid)
current_menu = guild['current_menu']
guild = await self.db.get_guild(gid, projection={'current_menu': 1})
channel = after.channel or before.channel
if not channel:
logging.info(f"[VOICE] No channel found for member {member.id}")
logging.warning(f"[VOICE] No channel found for member {member.id}")
return
vc = cast(discord.VoiceClient | None, discord.utils.get(self.typed_bot.voice_clients, guild=discord_guild))
vc = cast(discord.VoiceClient | None, discord.utils.get(self.typed_bot.voice_clients, guild=await self.typed_bot.fetch_guild(gid)))
if len(channel.members) == 1 and vc:
for member in channel.members:
if member.id == self.typed_bot.user.id: # type: ignore # should be logged in
logging.info(f"[VOICE] Voice state update for member {member.id} in guild {member.guild.id}")
break
else:
logging.debug(f"[VOICE] Bot is not in the channel {channel.id}")
return
if not vc:
logging.info(f"[VOICE] No voice client found for guild {gid}")
return
if len(channel.members) == 1:
logging.info(f"[VOICE] Clearing history and stopping playback for guild {gid}")
if member.guild.id in menu_views:
@@ -74,22 +81,11 @@ class Voice(Cog, VoiceExtension):
await message.delete()
await self.db.update(gid, {
'previous_tracks': [], 'next_tracks': [], 'votes': [],
'previous_tracks': [], 'next_tracks': [], 'votes': {},
'current_track': None, 'current_menu': None, 'vibing': False,
'repeat': False, 'shuffle': False, 'is_stopped': True
})
vc.stop()
elif len(channel.members) > 2 and not guild['always_allow_menu']:
if 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(current_menu)
await message.delete()
await channel.send("Меню отключено из-за большого количества участников.", delete_after=15)
except (discord.NotFound, discord.Forbidden):
pass
if member.guild.id in menu_views:
menu_views[member.guild.id].stop()
@@ -97,7 +93,7 @@ class Voice(Cog, VoiceExtension):
@Cog.listener()
async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent) -> None:
logging.info(f"[VOICE] Reaction added by user {payload.user_id} in channel {payload.channel_id}")
logging.debug(f"[VOICE] Reaction added by user {payload.user_id} in channel {payload.channel_id}")
if not self.typed_bot.user or not payload.member:
return
@@ -109,7 +105,15 @@ class Voice(Cog, VoiceExtension):
if not channel:
return
message = await channel.fetch_message(payload.message_id)
try:
message = await channel.fetch_message(payload.message_id)
except discord.Forbidden:
logging.info(f"[VOICE] Bot does not have permissions to read messages in channel {payload.channel_id}")
return
except discord.NotFound:
logging.info(f"[VOICE] Message {payload.message_id} not found in channel {payload.channel_id}")
return
if not message or message.author.id != bot_id:
return
@@ -122,7 +126,7 @@ class Voice(Cog, VoiceExtension):
if not guild_id:
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']
if str(payload.message_id) not in votes:
@@ -130,7 +134,6 @@ class Voice(Cog, VoiceExtension):
return
vote_data = votes[str(payload.message_id)]
logging.debug(f"[VOICE] Vote data for message {payload.message_id}: {vote_data}")
if payload.emoji.name == '':
logging.info(f"[VOICE] User {payload.user_id} voted positively for message {payload.message_id}")
@@ -143,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
if len(vote_data['positive_votes']) >= required_votes:
logging.info(f"[VOICE] Enough positive votes for message {payload.message_id}")
if vote_data['action'] == 'next':
logging.info(f"[VOICE] Skipping track for message {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)]
await message.delete()
await self.proccess_vote(payload, guild, channel, vote_data)
del votes[str(payload.message_id)]
elif len(vote_data['negative_votes']) >= required_votes:
logging.info(f"[VOICE] Enough negative votes for message {payload.message_id}")
@@ -202,13 +160,14 @@ class Voice(Cog, VoiceExtension):
@Cog.listener()
async def on_raw_reaction_remove(self, payload: discord.RawReactionActionEvent) -> None:
logging.info(f"[VOICE] Reaction removed by user {payload.user_id} in channel {payload.channel_id}")
logging.debug(f"[VOICE] Reaction removed by user {payload.user_id} in channel {payload.channel_id}")
if not self.typed_bot.user:
return
guild_id = payload.guild_id
if not guild_id:
return
guild = await self.db.get_guild(guild_id, projection={'votes': 1})
votes = guild['votes']
@@ -220,7 +179,15 @@ class Voice(Cog, VoiceExtension):
if not channel:
return
message = await channel.fetch_message(payload.message_id)
try:
message = await channel.fetch_message(payload.message_id)
except discord.Forbidden:
logging.info(f"[VOICE] Bot does not have permissions to read messages in channel {payload.channel_id}")
return
except discord.NotFound:
logging.info(f"[VOICE] Message {payload.message_id} not found in channel {payload.channel_id}")
return
if not message or message.author.id != self.typed_bot.user.id:
return
@@ -234,38 +201,33 @@ class Voice(Cog, VoiceExtension):
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:
logging.info(f"[VOICE] Menu 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})
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)
if await self.voice_check(ctx):
await self.send_menu_message(ctx)
@voice.command(name="join", description="Подключиться к голосовому каналу, в котором вы сейчас находитесь.")
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}")
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 = "У вас нет прав для выполнения этой команды."
elif (vc := await self.get_voice_client(ctx)) and vc.is_connected():
elif vc and vc.is_connected():
response_message = "❌ Бот уже находится в голосовом канале. Выключите его с помощью команды /voice leave."
elif isinstance(ctx.channel, discord.VoiceChannel):
await ctx.channel.connect(timeout=15)
response_message = "Подключение успешно!"
try:
await ctx.channel.connect()
except TimeoutError:
response_message = "Не удалось подключиться к голосовому каналу."
else:
response_message = "✅ Подключение успешно!"
else:
response_message = "❌ Вы должны отправить команду в голосовом канале."
response_message = "❌ Вы должны отправить команду в чате голосового канала."
logging.info(f"[VOICE] Join command response: {response_message}")
await ctx.respond(response_message, delete_after=15, ephemeral=True)
@@ -275,22 +237,22 @@ class Voice(Cog, VoiceExtension):
logging.info(f"[VOICE] Leave command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
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}")
await ctx.respond("У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True)
return
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)
if 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:
res = await self.stop_playing(ctx, vc=vc, full=True)
if not res:
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:
await ctx.respond("❌ Бот не подключен к голосовому каналу.", delete_after=15, ephemeral=True)
@@ -298,16 +260,38 @@ class Voice(Cog, VoiceExtension):
async def clear(self, ctx: discord.ApplicationContext) -> None:
logging.info(f"[VOICE] Clear queue command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
if not await self.voice_check(ctx):
return
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 execute leave command in guild {ctx.guild.id}")
await ctx.respond("У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True)
elif await self.voice_check(ctx):
await self.db.update(ctx.guild.id, {'previous_tracks': [], 'next_tracks': []})
await ctx.respond("Очередь и история сброшены.", delete_after=15, ephemeral=True)
logging.info(f"[VOICE] Queue and history cleared in guild {ctx.guild.id}")
logging.info(f"Starting vote for clearing queue in guild {ctx.guild.id}")
response_message = f"{member.mention} хочет очистить историю прослушивания и очередь треков.\n\n Выполнить действие?."
message = cast(discord.Interaction, await ctx.respond(response_message, delete_after=60))
response = await message.original_response()
await response.add_reaction('')
await response.add_reaction('')
await self.db.update_vote(
ctx.guild_id,
response.id,
{
'positive_votes': list(),
'negative_votes': list(),
'total_members': len(channel.members),
'action': 'clear_queue',
'vote_content': None
}
)
return
await self.db.update(ctx.guild.id, {'previous_tracks': [], 'next_tracks': []})
await ctx.respond("✅ Очередь и история сброшены.", delete_after=15, ephemeral=True)
logging.info(f"[VOICE] Queue and history cleared in guild {ctx.guild.id}")
@queue.command(description="Получить очередь треков.")
async def get(self, ctx: discord.ApplicationContext) -> None:
@@ -323,176 +307,44 @@ class Voice(Cog, VoiceExtension):
logging.info(f"[VOICE] Queue embed sent to user {ctx.author.id} in guild {ctx.guild.id}")
@track.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="Прервать проигрывание, удалить историю, очередь и текущий плеер.")
@voice.command(description="Прервать проигрывание, удалить историю, очередь и текущий плеер.")
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}")
if not await self.voice_check(ctx):
return
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} tried to stop playback in guild {ctx.guild.id} but there are other users in the channel")
await ctx.respond("❌ Вы не можете остановить воспроизведение, пока в канале находятся другие пользователи.", delete_after=15, ephemeral=True)
logging.info(f"Starting vote for stopping playback in guild {ctx.guild.id}")
elif await self.voice_check(ctx):
res = await self.stop_playing(ctx, full=True)
if res:
await ctx.respond("Воспроизведение остановлено.", delete_after=15, ephemeral=True)
else:
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_message = f"{member.mention} хочет полностью остановить проигрывание.\n\n Выполнить действие?."
message = cast(discord.Interaction, await ctx.respond(response_message, delete_after=60))
response = await message.original_response()
await response.add_reaction('')
await response.add_reaction('')
await self.db.update_vote(
gid,
ctx.guild_id,
response.id,
{
'positive_votes': list(),
'negative_votes': list(),
'total_members': len(channel.members),
'action': 'next',
'action': 'stop',
'vote_content': None
}
)
return
res = await self.stop_playing(ctx, full=True)
if res:
await ctx.respond("✅ Воспроизведение остановлено.", delete_after=15, ephemeral=True)
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)
await ctx.respond("❌ Произошла ошибка при остановке воспроизведения.", delete_after=15, ephemeral=True)
@voice.command(name='vibe', description="Запустить Мою Волну.")
@discord.option(
@@ -508,18 +360,14 @@ class Voice(Cog, VoiceExtension):
if not await self.voice_check(ctx):
return
guild = await self.db.get_guild(ctx.guild.id, projection={'always_allow_menu': 1, 'current_menu': 1, 'vibing': 1})
channel = cast(discord.VoiceChannel, ctx.channel)
guild = await self.db.get_guild(ctx.guild.id, projection={'current_menu': 1, 'vibing': 1})
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)
await ctx.respond("❌ Моя Волна уже включена. Используйте /voice stop, чтобы остановить воспроизведение.", delete_after=15, ephemeral=True)
return
await ctx.defer(invisible=False)
if name:
token = await users_db.get_ym_token(ctx.user.id)
if not token:
@@ -541,27 +389,64 @@ class Voice(Cog, VoiceExtension):
if not content:
logging.debug(f"[VOICE] Station {name} not found")
await ctx.respond("❌ Станция не найдена.", ephemeral=True)
await ctx.respond("❌ Станция не найдена.", delete_after=15, ephemeral=True)
return
_type, _id = content.ad_params.other_params.split(':') if content.ad_params else (None, None)
if not _type or not _id:
logging.debug(f"[VOICE] Station {name} has no ad params")
await ctx.respond("❌ Станция не найдена.", ephemeral=True)
await ctx.respond("❌ Станция не найдена.", delete_after=15, ephemeral=True)
return
else:
_type, _id = 'user', 'onyourwave'
content = None
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"Starting vote for starting vibe in guild {ctx.guild.id}")
if _type == 'user' and _id == 'onyourwave':
station = "Моя Волна"
elif content and content.station:
station = content.station.name
else:
logging.warning(f"[VOICE] Station {name} not found")
await ctx.respond("❌ Станция не найдена.", delete_after=15, ephemeral=True)
return
feedback = await self.update_vibe(ctx, _type, _id)
else:
feedback = await self.update_vibe(ctx, 'user', 'onyourwave')
response_message = f"{member.mention} хочет запустить станцию **{station}**.\n\n Выполнить действие?"
message = cast(discord.WebhookMessage, await ctx.respond(response_message))
if not feedback:
await ctx.respond("❌ Операция не удалась. Возможно, у вес нет подписки на Яндекс Музыку.", ephemeral=True)
await message.add_reaction('')
await message.add_reaction('')
await self.db.update_vote(
ctx.guild_id,
message.id,
{
'positive_votes': list(),
'negative_votes': list(),
'total_members': len(channel.members),
'action': 'vibe_station',
'vote_content': [_type, _id, ctx.user.id]
}
)
return
if not guild['current_menu']:
feedback = await self.update_vibe(ctx, _type, _id)
if not feedback:
await ctx.respond("❌ Операция не удалась. Возможно, у вес нет подписки на Яндекс Музыку.", delete_after=15, ephemeral=True)
return
if guild['current_menu']:
await ctx.respond("✅ Моя Волна включена.", delete_after=15, ephemeral=True)
else:
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)
await self._play_track(ctx, next_track)

View File

@@ -79,14 +79,10 @@ class BaseGuildsDatabase:
current_track=None,
current_menu=None,
is_stopped=True,
allow_explicit=True,
always_allow_menu=False,
allow_connect=True,
vote_next_track=True,
vote_add_track=True,
vote_add_album=True,
vote_add_artist=True,
vote_add_playlist=True,
allow_change_connect=True,
vote_switch_track=True,
vote_add=True,
shuffle=False,
repeat=False,
votes={},

View File

@@ -19,28 +19,29 @@ class VoiceGuildsDatabase(BaseGuildsDatabase):
if list_type not in ('next', 'previous', 'current'):
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'
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(
{'_id': gid},
update,
{'$pop': {field: -1}},
projection={field: 1},
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:
await guilds.find_one_and_update(
{'_id': gid},
{'$push': {'next_tracks': {'$each': [res], '$position': 0}}},
{'$push': {'next_tracks': {'$each': [guild['current_track']], '$position': 0}}},
projection={'next_tracks': 1}
)
return res
return res[0] if res else None
async def modify_track(
self,

View File

@@ -4,8 +4,11 @@ class MessageVotes(TypedDict):
positive_votes: list[int]
negative_votes: list[int]
total_members: int
action: Literal['next', 'add_track', 'add_album', 'add_artist', 'add_playlist']
vote_content: dict[str, Any] | list[dict[str, Any]] | None
action: Literal[
'next', 'play/pause', 'stop', 'repeat', 'shuffle', 'previous', 'add_track',
'add_album', 'add_artist', 'add_playlist', 'vibe_station', 'clear_queue'
]
vote_content: Any | None
class Guild(TypedDict, total=False):
next_tracks: list[dict[str, Any]]
@@ -13,14 +16,10 @@ class Guild(TypedDict, total=False):
current_track: dict[str, Any] | None
current_menu: int | None
is_stopped: bool
allow_explicit: bool
always_allow_menu: bool
allow_connect: bool
vote_next_track: bool
vote_add_track: bool
vote_add_album: bool
vote_add_artist: bool
vote_add_playlist: bool
allow_change_connect: bool
vote_switch_track: bool
vote_add: bool
shuffle: bool
repeat: bool
votes: dict[str, MessageVotes]
@@ -34,14 +33,10 @@ class ExplicitGuild(TypedDict):
current_track: dict[str, Any] | None
current_menu: int | None
is_stopped: bool # Prevents the `after` callback of play_track
allow_explicit: bool
always_allow_menu: bool
allow_connect: bool
vote_next_track: bool
vote_add_track: bool
vote_add_album: bool
vote_add_artist: bool
vote_add_playlist: bool
allow_change_connect: bool
vote_switch_track: bool
vote_add: bool
shuffle: bool
repeat: bool
votes: dict[str, MessageVotes]

View File

@@ -1,5 +1,5 @@
import logging
from typing import Any, Literal, cast
from typing import cast
import discord
from yandex_music import Track, Album, Artist, Playlist
@@ -27,40 +27,39 @@ class PlayButton(Button, VoiceExtension):
return
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)
member = cast(discord.Member, interaction.user)
action: Literal['add_track', 'add_album', 'add_artist', 'add_playlist']
if isinstance(self.item, Track):
tracks = [self.item]
action = 'add_track'
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):
album = await self.item.with_tracks_async()
if not album or not album.volumes:
logging.debug("[FIND] Failed to fetch album tracks in PlayButton callback")
await interaction.respond("Не удалось получить треки альбома.", ephemeral=True)
await interaction.respond("Не удалось получить треки альбома.", ephemeral=True)
return
tracks = [track for volume in album.volumes for track in volume]
action = 'add_album'
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):
artist_tracks = await self.item.get_tracks_async()
if not artist_tracks:
logging.debug("[FIND] Failed to fetch artist tracks in PlayButton callback")
await interaction.respond("Не удалось получить треки артиста.", ephemeral=True)
await interaction.respond("Не удалось получить треки артиста.", ephemeral=True)
return
tracks = artist_tracks.tracks.copy()
action = 'add_artist'
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):
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]
action = 'add_playlist'
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):
tracks = self.item.copy()
@@ -83,15 +82,15 @@ class PlayButton(Button, VoiceExtension):
action = 'add_playlist'
vote_message = f"{member.mention} хочет добавить плейлист **Мне Нравится** в очередь.\n\n Голосуйте за добавление."
response_message = f"Плейлист **«Мне нравится»** был добавлен в очередь."
response_message = f"Плейлист **«Мне нравится»** был добавлен в очередь."
else:
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:
logging.debug(f"Starting vote for '{action}' (from PlayButton callback)")
if guild['vote_add'] and len(channel.members) > 2 and not member.guild_permissions.manage_channels:
logging.info(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=60))
response = await message.original_response()
await response.add_reaction('')
@@ -108,26 +107,28 @@ class PlayButton(Button, VoiceExtension):
'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:
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:
logging.debug(f"[FIND] Adding tracks to queue (from PlayButton callback)")
await self.db.modify_track(gid, tracks, 'next', 'extend')
else:
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)
if interaction.message:
await interaction.message.delete()
else:
logging.warning(f"[FIND] Interaction message is None")
class MyVibeButton(Button, VoiceExtension):
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")
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: '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')
if next_track:
await self._play_next_track(interaction, next_track)
await self._play_track(interaction, next_track)
class ListenView(View):
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()
except discord.NotFound:
pass
self.stop()

View File

@@ -2,7 +2,7 @@ import logging
from typing import Self, cast
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
from yandex_music import TrackLyrics, Playlist, ClientAsync as YMClient
@@ -13,14 +13,14 @@ class ToggleButton(Button, VoiceExtension):
super().__init__(*args, **kwargs)
VoiceExtension.__init__(self, None)
async def callback(self, interaction: Interaction):
async def callback(self, interaction: Interaction) -> None:
callback_type = interaction.custom_id
if callback_type not in ('repeat', 'shuffle'):
raise ValueError(f"Invalid callback type: '{callback_type}'")
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.')
await interaction.respond("❌ Что-то пошло не так. Попробуйте снова.", delete_after=15, ephemeral=True)
return
@@ -29,9 +29,36 @@ class ToggleButton(Button, VoiceExtension):
return
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]})
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)
class PlayPauseButton(Button, VoiceExtension):
@@ -44,9 +71,39 @@ class PlayPauseButton(Button, VoiceExtension):
if not await self.voice_check(interaction, check_vibe_privilage=True):
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:
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:
embed = interaction.message.embeds[0]
except IndexError:
@@ -67,23 +124,61 @@ class SwitchTrackButton(Button, VoiceExtension):
super().__init__(*args, **kwargs)
VoiceExtension.__init__(self, None)
async def callback(self, interaction: Interaction):
async def callback(self, interaction: Interaction) -> None:
callback_type = interaction.custom_id
if callback_type not in ('next', 'previous'):
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')
if not await self.voice_check(interaction, check_vibe_privilage=True):
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':
title = await self.next_track(interaction, button_callback=True)
else:
title = await self.prev_track(interaction, button_callback=True)
title = await self.previous_track(interaction, button_callback=True)
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):
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:
await interaction.respond("❌ Нет воспроизводимого трека.", delete_after=15, ephemeral=True)
channel = cast(VoiceChannel, interaction.channel)
res = await self.react_track(interaction, callback_type)
if callback_type == 'like' and res[0]:
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
)
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:
logging.debug(f"[VC_EXT] Failed to {callback_type} track")
await interaction.respond("❌ Операция не удалась. Попробуйте позже.")
logging.debug(f"[VC_EXT] Failed to get {callback_type} tracks")
await interaction.respond("❌ Операция не удалась. Попробуйте позже.", delete_after=15, ephemeral=True)
class LyricsButton(Button, VoiceExtension):
def __init__(self, **kwargs):
@@ -192,7 +303,7 @@ class MyVibeButton(Button, VoiceExtension):
if next_track:
# Need to avoid additional feedback.
# 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):
def __init__(self, *args, **kwargs):
@@ -211,26 +322,26 @@ class MyVibeSelect(Select, VoiceExtension):
custom_id = interaction.custom_id
if custom_id not in ('diversity', 'mood', 'lang'):
logging.warning(f'[MENU] Unknown custom_id: {custom_id}')
logging.error(f'[MENU] Unknown custom_id: {custom_id}')
return
if not interaction.data or 'values' not in interaction.data:
if not interaction.data:
logging.warning('[MENU] No data in select callback')
return
data_value = interaction.data['values'][0]
if data_value not in (
data_values = cast(list[str] | None, interaction.data.get('values'))
if not data_values or data_values[0] not in (
'fun', 'active', 'calm', 'sad', 'all',
'favorite', 'popular', 'discover', 'default',
'not-russian', 'russian', 'without-words', 'any'
):
logging.warning(f'[MENU] Unknown data_value: {data_value}')
logging.error(f'[MENU] Unknown data_value: {data_values}')
return
logging.info(f"[MENU] Settings option '{custom_id}' updated to {data_value}")
await self.users_db.update(interaction.user.id, {f'vibe_settings.{custom_id}': 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_values[0]})
view = MyVibeSettingsView(interaction)
view = await MyVibeSettingsView(interaction).init()
view.disable_all_items()
await interaction.edit(view=view)
@@ -330,10 +441,15 @@ class AddToPlaylistSelect(Select, VoiceExtension):
logging.warning('[MENU] No data in select callback')
return
data = interaction.data['values'][0].split(';')
logging.debug(f"[MENU] Add to playlist select callback: {data}")
data_values = cast(list[str] | None, interaction.data.get('values'))
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')
if not current_track:
@@ -362,14 +478,19 @@ class AddToPlaylistButton(Button, VoiceExtension):
return
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:
await interaction.respond('❌ Что-то пошло не так. Попробуйте позже.', ephemeral=True)
if not client:
await interaction.respond('❌ Что-то пошло не так. Попробуйте позже.', delete_after=15, ephemeral=True)
return
if not (vc := await self.get_voice_client(interaction)) or not vc.is_playing:
await interaction.respond("❌ Нет воспроизводимого трека.", delete_after=15, ephemeral=True)
return
playlists = await client.users_playlists_list()
if not playlists:
await interaction.respond('У вас нет плейлистов.', delete_after=15, ephemeral=True)
return
view = View(
AddToPlaylistSelect(
client,
@@ -379,7 +500,7 @@ class AddToPlaylistButton(Button, VoiceExtension):
SelectOption(
label=playlist.title or "Без названия",
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.shuffle_button)
if not isinstance(self.ctx, RawReactionActionEvent) and len(cast(VoiceChannel, self.ctx.channel).members) > 2:
self.dislike_button.disabled = True
elif likes and current_track and str(current_track['id']) in [str(like.id) for like in likes]:
self.like_button.style = ButtonStyle.success
if not isinstance(self.ctx, RawReactionActionEvent) and len(cast(VoiceChannel, self.ctx.channel).members) == 2:
if likes and current_track and str(current_track['id']) in [str(like.id) for like in likes]:
self.like_button.style = ButtonStyle.success
if not current_track:
self.lyrics_button.disabled = True
@@ -469,3 +589,4 @@ class MenuView(View, VoiceExtension):
logging.debug('[MENU] Successfully deleted menu message')
else:
logging.debug('[MENU] No menu message found')
self.stop()