mirror of
https://github.com/deadcxap/YandexMusicDiscordBot.git
synced 2026-01-10 02:31:44 +03:00
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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={},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user