mirror of
https://github.com/deadcxap/YandexMusicDiscordBot.git
synced 2026-01-10 02:41:45 +03:00
@@ -12,17 +12,18 @@ from yandex_music import Track, Album, Artist, Playlist
|
|||||||
|
|
||||||
from MusicBot.database import BaseUsersDatabase, BaseGuildsDatabase
|
from MusicBot.database import BaseUsersDatabase, BaseGuildsDatabase
|
||||||
|
|
||||||
from MusicBot.ui import ListenView, MyPlaylists, generate_playlists_embed
|
from MusicBot.ui import ListenView
|
||||||
from MusicBot.cogs.utils.embeds import generate_item_embed
|
from MusicBot.cogs.utils.embeds import generate_item_embed
|
||||||
|
|
||||||
|
users_db = BaseUsersDatabase()
|
||||||
|
|
||||||
def setup(bot):
|
def setup(bot):
|
||||||
bot.add_cog(General(bot))
|
bot.add_cog(General(bot))
|
||||||
|
|
||||||
async def get_search_suggestions(ctx: discord.AutocompleteContext) -> list[str]:
|
async def get_search_suggestions(ctx: discord.AutocompleteContext) -> list[str]:
|
||||||
if not ctx.interaction.user or not ctx.value or len(ctx.value) < 2:
|
if not ctx.interaction.user or not ctx.value or len(ctx.value) < 2:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
users_db = BaseUsersDatabase()
|
|
||||||
token = await users_db.get_ym_token(ctx.interaction.user.id)
|
token = await users_db.get_ym_token(ctx.interaction.user.id)
|
||||||
if not token:
|
if not token:
|
||||||
logging.info(f"[GENERAL] User {ctx.interaction.user.id} has no token")
|
logging.info(f"[GENERAL] User {ctx.interaction.user.id} has no token")
|
||||||
@@ -56,20 +57,35 @@ async def get_search_suggestions(ctx: discord.AutocompleteContext) -> list[str]:
|
|||||||
for item in search.playlists.results:
|
for item in search.playlists.results:
|
||||||
res.append(f"{item.title}")
|
res.append(f"{item.title}")
|
||||||
elif content_type == "Свой плейлист":
|
elif content_type == "Свой плейлист":
|
||||||
if not client.me or not client.me.account or not client.me.account.uid:
|
playlists_list = await client.users_playlists_list()
|
||||||
logging.warning(f"Failed to get playlists for user {ctx.interaction.user.id}")
|
res = [playlist.title if playlist.title else 'Без названия' for playlist in playlists_list]
|
||||||
else:
|
|
||||||
playlists_list = await client.users_playlists_list(client.me.account.uid)
|
|
||||||
res = [playlist.title if playlist.title else 'Без названия' for playlist in playlists_list]
|
|
||||||
|
|
||||||
return res[:100]
|
return res[:100]
|
||||||
|
|
||||||
|
async def get_user_playlists_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)
|
||||||
|
if not token:
|
||||||
|
logging.info(f"[GENERAL] User {ctx.interaction.user.id} 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")
|
||||||
|
return []
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
class General(Cog):
|
class General(Cog):
|
||||||
|
|
||||||
def __init__(self, bot: discord.Bot):
|
def __init__(self, bot: discord.Bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.db = BaseGuildsDatabase()
|
self.db = BaseGuildsDatabase()
|
||||||
self.users_db = BaseUsersDatabase()
|
self.users_db = users_db
|
||||||
|
|
||||||
account = discord.SlashCommandGroup("account", "Команды, связанные с аккаунтом.")
|
account = discord.SlashCommandGroup("account", "Команды, связанные с аккаунтом.")
|
||||||
|
|
||||||
@@ -106,7 +122,6 @@ class General(Cog):
|
|||||||
value="""`account`
|
value="""`account`
|
||||||
`find`
|
`find`
|
||||||
`help`
|
`help`
|
||||||
`like`
|
|
||||||
`queue`
|
`queue`
|
||||||
`settings`
|
`settings`
|
||||||
`track`
|
`track`
|
||||||
@@ -116,26 +131,23 @@ class General(Cog):
|
|||||||
embed.set_footer(text='©️ Bananchiki')
|
embed.set_footer(text='©️ Bananchiki')
|
||||||
elif command == 'account':
|
elif command == 'account':
|
||||||
embed.description += (
|
embed.description += (
|
||||||
"Ввести токен от Яндекс Музыки. Его можно получить [здесь](https://github.com/MarshalX/yandex-music-api/discussions/513).\n"
|
"Ввести токен Яндекс Музыки. Его можно получить [здесь](https://github.com/MarshalX/yandex-music-api/discussions/513).\n"
|
||||||
"```/account login <token>```\n"
|
"```/account login <token>```\n"
|
||||||
"Удалить токен из базы данных бота.\n```/account remove```\n"
|
"Удалить токен из базы данных бота.\n```/account remove```\n"
|
||||||
"Получить ваши плейлисты. Чтобы добавить плейлист в очередь, используйте команду /find.\n```/account playlists```\n"
|
"Получить ваш плейлист.\n```/account playlist <название>```\n"
|
||||||
"Получить плейлист «Мне нравится».\n```/account likes```\n"
|
"Получить плейлист «Мне нравится».\n```/account likes```\n"
|
||||||
|
"Получить ваши рекомендации.\n```/account recommendations <тип>```\n"
|
||||||
)
|
)
|
||||||
elif command == 'find':
|
elif command == 'find':
|
||||||
embed.description += (
|
embed.description += (
|
||||||
"Вывести информацию о треке (по умолчанию), альбоме, авторе или плейлисте. Позволяет добавить музыку в очередь. "
|
"Вывести информацию о треке (по умолчанию), альбоме, авторе или плейлисте. Позволяет добавить музыку в очередь. "
|
||||||
"В названии можно уточнить автора или версию. Возвращается лучшее совпадение.\n```/find <название> <тип>```"
|
"В названии можно уточнить автора через «-». Возвращается лучшее совпадение.\n```/find <тип> <название>```"
|
||||||
)
|
)
|
||||||
elif command == 'help':
|
elif command == 'help':
|
||||||
embed.description += (
|
embed.description += (
|
||||||
"Вывести список всех команд.\n```/help```\n"
|
"Вывести список всех команд.\n```/help```\n"
|
||||||
"Получить информацию о конкретной команде.\n```/help <команда>```"
|
"Получить информацию о конкретной команде.\n```/help <команда>```"
|
||||||
)
|
)
|
||||||
elif command == 'like':
|
|
||||||
embed.description += (
|
|
||||||
"Добавить трек в плейлист «Мне нравится». Пользовательские треки из этого плейлиста игнорируются.\n```/like```"
|
|
||||||
)
|
|
||||||
elif command == 'queue':
|
elif command == 'queue':
|
||||||
embed.description += (
|
embed.description += (
|
||||||
"Получить очередь треков. По 15 элементов на страницу.\n```/queue get```\n"
|
"Получить очередь треков. По 15 элементов на страницу.\n```/queue get```\n"
|
||||||
@@ -144,11 +156,12 @@ class General(Cog):
|
|||||||
)
|
)
|
||||||
elif command == 'settings':
|
elif command == 'settings':
|
||||||
embed.description += (
|
embed.description += (
|
||||||
|
"`Примечание`: Только пользователи с разрешением управления каналом могут менять настройки.\n\n"
|
||||||
"Получить текущие настройки.\n```/settings show```\n"
|
"Получить текущие настройки.\n```/settings show```\n"
|
||||||
"Разрешить или запретить воспроизведение Explicit треков и альбомов. Если автор или плейлист содержат Explicit треки, убираются кнопки для доступа к ним.\n```/settings explicit```\n"
|
"Разрешить или запретить воспроизведение Explicit треков и альбомов. Если автор или плейлист содержат Explicit треки, убираются кнопки для доступа к ним.\n```/settings explicit```\n"
|
||||||
"Разрешить или запретить создание меню проигрывателя, когда в канале больше одного человека.\n```/settings menu```\n"
|
"Разрешить или запретить создание меню проигрывателя, когда в канале больше одного человека.\n```/settings menu```\n"
|
||||||
"Разрешить или запретить голосование.\n```/settings vote <тип голосования>```\n"
|
"Разрешить или запретить голосование.\n```/settings vote <тип>```\n"
|
||||||
"`Примечание`: Только пользователи с разрешением управления каналом могут менять настройки."
|
"Разрешить или запретить отключение/подключение бота к каналу участникам без прав управления каналом.\n```/settings connect```\n"
|
||||||
)
|
)
|
||||||
elif command == 'track':
|
elif command == 'track':
|
||||||
embed.description += (
|
embed.description += (
|
||||||
@@ -157,15 +170,16 @@ class General(Cog):
|
|||||||
"Приостановить текущий трек.\n```/track pause```\n"
|
"Приостановить текущий трек.\n```/track pause```\n"
|
||||||
"Возобновить текущий трек.\n```/track resume```\n"
|
"Возобновить текущий трек.\n```/track resume```\n"
|
||||||
"Прервать проигрывание, удалить историю, очередь и текущий плеер.\n ```/track stop```\n"
|
"Прервать проигрывание, удалить историю, очередь и текущий плеер.\n ```/track stop```\n"
|
||||||
|
"Добавить трек в плейлист «Мне нравится» или удалить его, если он уже там.\n```/track like```"
|
||||||
"Запустить Мою Волну по текущему треку.\n```/track vibe```"
|
"Запустить Мою Волну по текущему треку.\n```/track vibe```"
|
||||||
)
|
)
|
||||||
elif command == 'voice':
|
elif command == 'voice':
|
||||||
embed.description += (
|
embed.description += (
|
||||||
"`Примечание`: Доступность меню и Моей Волны зависит от настроек сервера.\n\n"
|
"`Примечание`: Доступность меню и Моей Волны зависит от настроек сервера.\n\n"
|
||||||
"Присоединить бота в голосовой канал. Требует разрешения управления каналом.\n```/voice join```\n"
|
"Присоединить бота в голосовой канал.\n```/voice join```\n"
|
||||||
"Заставить бота покинуть голосовой канал. Требует разрешения управления каналом.\n ```/voice leave```\n"
|
"Заставить бота покинуть голосовой канал.\n ```/voice leave```\n"
|
||||||
"Создать меню проигрывателя. По умолчанию работает только когда в канале один человек.\n```/voice menu```\n"
|
"Создать меню проигрывателя. \n```/voice menu```\n"
|
||||||
"Запустить Мою Волну. По умолчанию работает только когда в канале один человек.\n```/vibe```"
|
"Запустить станцию. Без уточнения станции, запускает Мою Волну.\n```/voice vibe <название станции>```"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
response_message = '❌ Неизвестная команда.'
|
response_message = '❌ Неизвестная команда.'
|
||||||
@@ -173,7 +187,7 @@ class General(Cog):
|
|||||||
|
|
||||||
await ctx.respond(response_message, embed=embed, ephemeral=True)
|
await ctx.respond(response_message, embed=embed, ephemeral=True)
|
||||||
|
|
||||||
@account.command(description="Ввести токен от Яндекс Музыки.")
|
@account.command(description="Ввести токен Яндекс Музыки.")
|
||||||
@discord.option("token", type=discord.SlashCommandOptionType.string, description="Токен.")
|
@discord.option("token", type=discord.SlashCommandOptionType.string, description="Токен.")
|
||||||
async def login(self, ctx: discord.ApplicationContext, token: str) -> None:
|
async def login(self, ctx: discord.ApplicationContext, token: str) -> None:
|
||||||
logging.info(f"[GENERAL] Login command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
|
logging.info(f"[GENERAL] Login command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
|
||||||
@@ -190,9 +204,14 @@ class General(Cog):
|
|||||||
logging.info(f"[GENERAL] Token saved for user {ctx.author.id}")
|
logging.info(f"[GENERAL] Token saved for user {ctx.author.id}")
|
||||||
await ctx.respond(f'Привет, {about['account']['first_name']}!', delete_after=15, ephemeral=True)
|
await ctx.respond(f'Привет, {about['account']['first_name']}!', delete_after=15, ephemeral=True)
|
||||||
|
|
||||||
@account.command(description="Удалить токен из датабазы бота.")
|
@account.command(description="Удалить токен из базы данных бота.")
|
||||||
async def remove(self, ctx: discord.ApplicationContext) -> None:
|
async def remove(self, ctx: discord.ApplicationContext) -> None:
|
||||||
logging.info(f"[GENERAL] Remove command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
|
logging.info(f"[GENERAL] Remove command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
|
||||||
|
if not await self.users_db.get_ym_token(ctx.user.id):
|
||||||
|
logging.info(f"[GENERAL] No token found for user {ctx.author.id}")
|
||||||
|
await ctx.respond('❌ Токен не указан.', delete_after=15, ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
await self.users_db.update(ctx.user.id, {'ym_token': None})
|
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)
|
||||||
|
|
||||||
@@ -228,30 +247,111 @@ class General(Cog):
|
|||||||
logging.info(f"[GENERAL] Successfully fetched likes for user {ctx.user.id}")
|
logging.info(f"[GENERAL] Successfully fetched likes for user {ctx.user.id}")
|
||||||
await ctx.respond(embed=embed, view=ListenView(tracks))
|
await ctx.respond(embed=embed, view=ListenView(tracks))
|
||||||
|
|
||||||
@account.command(description="Получить ваши плейлисты.")
|
@account.command(description="Получить ваши рекомендации.")
|
||||||
async def playlists(self, ctx: discord.ApplicationContext) -> None:
|
@discord.option(
|
||||||
logging.info(f"[GENERAL] Playlists command invoked by user {ctx.user.id} in guild {ctx.guild_id}")
|
'тип',
|
||||||
|
parameter_name='content_type',
|
||||||
|
description="Вид рекомендаций.",
|
||||||
|
type=discord.SlashCommandOptionType.string,
|
||||||
|
choices=['Премьера', 'Плейлист дня', 'Дежавю']
|
||||||
|
)
|
||||||
|
async def recommendations(
|
||||||
|
self,
|
||||||
|
ctx: discord.ApplicationContext,
|
||||||
|
content_type: Literal['Премьера', 'Плейлист дня', 'Дежавю']
|
||||||
|
)-> None:
|
||||||
|
# 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)
|
token = await self.users_db.get_ym_token(ctx.user.id)
|
||||||
if not token:
|
if not token:
|
||||||
await ctx.respond("❌ Укажите токен через /account login.", delete_after=15, ephemeral=True)
|
await ctx.respond("❌ Укажите токен через /account login.", delete_after=15, ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
client = await YMClient(token).init()
|
client = await YMClient(token).init()
|
||||||
if not client.me or not client.me.account or not client.me.account.uid:
|
|
||||||
|
search = await client.search(content_type, False, '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)
|
await ctx.respond('❌ Что-то пошло не так. Повторите попытку позже.', delete_after=15, ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
playlists_list = await client.users_playlists_list(client.me.account.uid)
|
playlist = search.playlists.results[0]
|
||||||
playlists: list[tuple[str, int]] = [
|
if playlist is None:
|
||||||
(playlist.title if playlist.title else 'Без названия', playlist.track_count if playlist.track_count else 0) for playlist in playlists_list
|
logging.info(f"[GENERAL] Failed to fetch recommendations for user {ctx.user.id}")
|
||||||
]
|
await ctx.respond('❌ Что-то пошло не так. Повторите попытку позже.', delete_after=15, ephemeral=True)
|
||||||
|
|
||||||
await self.users_db.update(ctx.user.id, {'playlists': playlists, 'playlists_page': 0})
|
tracks = await playlist.fetch_tracks_async()
|
||||||
embed = generate_playlists_embed(0, playlists)
|
if not tracks:
|
||||||
|
logging.info(f"[GENERAL] User {ctx.user.id} search for '{content_type}' returned no tracks")
|
||||||
|
await ctx.respond("❌ Пустой плейлист.", delete_after=15, ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
logging.info(f"[GENERAL] Successfully fetched playlists for user {ctx.user.id}")
|
embed = await generate_item_embed(playlist)
|
||||||
await ctx.respond(embed=embed, view=await MyPlaylists(ctx).init(), ephemeral=True)
|
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)
|
||||||
|
|
||||||
|
@account.command(description="Получить ваш плейлист.")
|
||||||
|
@discord.option(
|
||||||
|
"запрос",
|
||||||
|
parameter_name='name',
|
||||||
|
description="Название плейлиста.",
|
||||||
|
type=discord.SlashCommandOptionType.string,
|
||||||
|
autocomplete=discord.utils.basic_autocomplete(get_user_playlists_suggestions)
|
||||||
|
)
|
||||||
|
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}")
|
||||||
|
await ctx.respond("❌ Укажите токен через /account login.", delete_after=15, ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = await YMClient(token).init()
|
||||||
|
except yandex_music.exceptions.UnauthorizedError:
|
||||||
|
logging.info(f"[GENERAL] User {ctx.user.id} provided invalid token")
|
||||||
|
await ctx.respond("❌ Недействительный токен. Если это не так, попробуйте ещё раз.", delete_after=15, ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
playlists = await client.users_playlists_list()
|
||||||
|
|
||||||
|
playlist = next((playlist for playlist in playlists if playlist.title == name), None)
|
||||||
|
if not playlist:
|
||||||
|
logging.info(f"[GENERAL] User {ctx.user.id} playlist '{name}' not found")
|
||||||
|
await ctx.respond("❌ Плейлист не найден.", delete_after=15, ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
tracks = await playlist.fetch_tracks_async()
|
||||||
|
if not tracks:
|
||||||
|
logging.info(f"[GENERAL] User {ctx.user.id} playlist '{name}' is empty")
|
||||||
|
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)
|
||||||
|
|
||||||
@discord.slash_command(description="Найти контент и отправить информацию о нём. Возвращается лучшее совпадение.")
|
@discord.slash_command(description="Найти контент и отправить информацию о нём. Возвращается лучшее совпадение.")
|
||||||
@discord.option(
|
@discord.option(
|
||||||
@@ -259,7 +359,7 @@ class General(Cog):
|
|||||||
parameter_name='content_type',
|
parameter_name='content_type',
|
||||||
description="Тип контента для поиска.",
|
description="Тип контента для поиска.",
|
||||||
type=discord.SlashCommandOptionType.string,
|
type=discord.SlashCommandOptionType.string,
|
||||||
choices=['Трек', 'Альбом', 'Артист', 'Плейлист', 'Свой плейлист'],
|
choices=['Трек', 'Альбом', 'Артист', 'Плейлист'],
|
||||||
)
|
)
|
||||||
@discord.option(
|
@discord.option(
|
||||||
"запрос",
|
"запрос",
|
||||||
@@ -271,9 +371,11 @@ class General(Cog):
|
|||||||
async def find(
|
async def find(
|
||||||
self,
|
self,
|
||||||
ctx: discord.ApplicationContext,
|
ctx: discord.ApplicationContext,
|
||||||
content_type: Literal['Трек', 'Альбом', 'Артист', 'Плейлист', 'Свой плейлист'],
|
content_type: Literal['Трек', 'Альбом', 'Артист', 'Плейлист'],
|
||||||
name: str
|
name: str
|
||||||
) -> None:
|
) -> 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}'")
|
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})
|
guild = await self.db.get_guild(ctx.guild_id, projection={'allow_explicit': 1})
|
||||||
@@ -290,89 +392,61 @@ class General(Cog):
|
|||||||
await ctx.respond("❌ Недействительный токен. Если это не так, попробуйте ещё раз.", delete_after=15, ephemeral=True)
|
await ctx.respond("❌ Недействительный токен. Если это не так, попробуйте ещё раз.", delete_after=15, ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
if content_type == 'Свой плейлист':
|
result = await client.search(name, nocorrect=True)
|
||||||
if not client.me or not client.me.account or not client.me.account.uid:
|
|
||||||
logging.warning(f"Failed to get user info for user {ctx.user.id}")
|
if not result:
|
||||||
await ctx.respond("❌ Не удалось получить информацию о пользователе.", delete_after=15, ephemeral=True)
|
logging.warning(f"Failed to search for '{name}' for user {ctx.user.id}")
|
||||||
return
|
await ctx.respond("❌ Что-то пошло не так. Повторите попытку позже.", delete_after=15, ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
playlists = await client.users_playlists_list(client.me.account.uid)
|
if content_type == 'Трек':
|
||||||
result = next((playlist for playlist in playlists if playlist.title == name), None)
|
content = result.tracks
|
||||||
if not result:
|
elif content_type == 'Альбом':
|
||||||
logging.info(f"[GENERAL] User {ctx.user.id} playlist '{name}' not found")
|
content = result.albums
|
||||||
await ctx.respond("❌ Плейлист не найден.", delete_after=15, ephemeral=True)
|
elif content_type == 'Артист':
|
||||||
return
|
content = result.artists
|
||||||
|
elif content_type == 'Плейлист':
|
||||||
tracks = await result.fetch_tracks_async()
|
content = 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:
|
if not tracks:
|
||||||
logging.info(f"[GENERAL] User {ctx.user.id} playlist '{name}' is empty")
|
logging.info(f"[GENERAL] User {ctx.user.id} search for '{name}' returned no tracks")
|
||||||
await ctx.respond("❌ Плейлист пуст.", delete_after=15, ephemeral=True)
|
await ctx.respond("❌ Треки от этого исполнителя не найдены.", delete_after=15, ephemeral=True)
|
||||||
return
|
return
|
||||||
|
for track in tracks:
|
||||||
for track_short 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)
|
track = cast(Track, track_short.track)
|
||||||
if (track.explicit or track.content_warning) and not guild['allow_explicit']:
|
if (track.explicit or track.content_warning) and not guild['allow_explicit']:
|
||||||
logging.info(f"[GENERAL] User {ctx.user.id} playlist '{name}' contains explicit content and is not allowed on this server")
|
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)
|
view = None
|
||||||
return
|
embed.set_footer(text="Воспроизведение недоступно, так как в плейлисте присутствуют Explicit треки")
|
||||||
|
break
|
||||||
embed = await generate_item_embed(result)
|
|
||||||
view = ListenView(result)
|
|
||||||
else:
|
|
||||||
result = await client.search(name, nocorrect=True)
|
|
||||||
|
|
||||||
if not 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
|
|
||||||
elif content_type == 'Альбом':
|
|
||||||
content = result.albums
|
|
||||||
elif content_type == 'Артист':
|
|
||||||
content = result.artists
|
|
||||||
elif content_type == 'Плейлист':
|
|
||||||
content = 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
|
|
||||||
|
|
||||||
logging.info(f"[GENERAL] Successfully generated '{content_type}' message for user {ctx.author.id}")
|
logging.info(f"[GENERAL] Successfully generated '{content_type}' message for user {ctx.author.id}")
|
||||||
await ctx.respond(embed=embed, view=view)
|
await ctx.respond(embed=embed, view=view)
|
||||||
|
|||||||
@@ -99,10 +99,13 @@ async def _generate_track_embed(track: Track) -> Embed:
|
|||||||
|
|
||||||
artist_url = f"https://music.yandex.ru/artist/{artist.id}"
|
artist_url = f"https://music.yandex.ru/artist/{artist.id}"
|
||||||
artist_cover = artist.cover
|
artist_cover = artist.cover
|
||||||
if not artist_cover:
|
|
||||||
|
if not artist_cover and artist.op_image:
|
||||||
artist_cover_url = artist.get_op_image_url()
|
artist_cover_url = artist.get_op_image_url()
|
||||||
else:
|
elif artist_cover:
|
||||||
artist_cover_url = artist_cover.get_url()
|
artist_cover_url = artist_cover.get_url()
|
||||||
|
else:
|
||||||
|
artist_cover_url = None
|
||||||
|
|
||||||
embed = Embed(
|
embed = Embed(
|
||||||
title=title,
|
title=title,
|
||||||
@@ -172,10 +175,13 @@ async def _generate_album_embed(album: Album) -> Embed:
|
|||||||
|
|
||||||
artist_url = f"https://music.yandex.ru/artist/{artist.id}"
|
artist_url = f"https://music.yandex.ru/artist/{artist.id}"
|
||||||
artist_cover = artist.cover
|
artist_cover = artist.cover
|
||||||
if not artist_cover:
|
|
||||||
|
if not artist_cover and artist.op_image:
|
||||||
artist_cover_url = artist.get_op_image_url()
|
artist_cover_url = artist.get_op_image_url()
|
||||||
else:
|
elif artist_cover:
|
||||||
artist_cover_url = artist_cover.get_url()
|
artist_cover_url = artist_cover.get_url()
|
||||||
|
else:
|
||||||
|
artist_cover_url = None
|
||||||
|
|
||||||
embed = Embed(
|
embed = Embed(
|
||||||
title=title,
|
title=title,
|
||||||
@@ -264,26 +270,28 @@ async def _generate_playlist_embed(playlist: Playlist) -> Embed:
|
|||||||
title = cast(str, playlist.title)
|
title = cast(str, playlist.title)
|
||||||
track_count = playlist.track_count
|
track_count = playlist.track_count
|
||||||
avail = cast(bool, playlist.available)
|
avail = cast(bool, playlist.available)
|
||||||
description = playlist.description_formatted
|
description = playlist.description
|
||||||
year = playlist.created
|
year = playlist.created
|
||||||
modified = playlist.modified
|
modified = playlist.modified
|
||||||
duration = playlist.duration_ms
|
duration = playlist.duration_ms
|
||||||
likes_count = playlist.likes_count
|
likes_count = playlist.likes_count
|
||||||
|
|
||||||
color = 0x000
|
|
||||||
cover_url = None
|
cover_url = None
|
||||||
|
|
||||||
if playlist.cover and playlist.cover.uri:
|
if playlist.cover and playlist.cover.uri:
|
||||||
cover_url = f"https://{playlist.cover.uri.replace('%%', '400x400')}"
|
cover_url = f"https://{playlist.cover.uri.replace('%%', '400x400')}"
|
||||||
else:
|
else:
|
||||||
tracks = await playlist.fetch_tracks_async()
|
tracks = await playlist.fetch_tracks_async()
|
||||||
for i in range(len(tracks)):
|
for track_short in tracks:
|
||||||
track = tracks[i].track
|
track = track_short.track
|
||||||
if not track or not track.albums or not track.albums[0].cover_uri:
|
if track and track.albums and track.albums[0].cover_uri:
|
||||||
continue
|
cover_url = f"https://{track.albums[0].cover_uri.replace('%%', '400x400')}"
|
||||||
|
break
|
||||||
|
|
||||||
if cover_url:
|
if cover_url:
|
||||||
color = await _get_average_color_from_url(cover_url)
|
color = await _get_average_color_from_url(cover_url)
|
||||||
|
else:
|
||||||
|
color = 0x000
|
||||||
|
|
||||||
embed = Embed(
|
embed = Embed(
|
||||||
title=title,
|
title=title,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import aiofiles
|
|||||||
import logging
|
import logging
|
||||||
import io
|
import io
|
||||||
from typing import Any, Literal, cast
|
from typing import Any, Literal, cast
|
||||||
from time import time
|
|
||||||
|
|
||||||
import yandex_music.exceptions
|
import yandex_music.exceptions
|
||||||
from yandex_music import Track, TrackShort, ClientAsync as YMClient
|
from yandex_music import Track, TrackShort, ClientAsync as YMClient
|
||||||
@@ -24,16 +23,17 @@ class VoiceExtension:
|
|||||||
self.db = VoiceGuildsDatabase()
|
self.db = VoiceGuildsDatabase()
|
||||||
self.users_db = BaseUsersDatabase()
|
self.users_db = BaseUsersDatabase()
|
||||||
|
|
||||||
async def send_menu_message(self, ctx: ApplicationContext | Interaction) -> bool:
|
async def send_menu_message(self, ctx: ApplicationContext | Interaction, *, disable: bool = False) -> bool:
|
||||||
"""Send menu message to the channel and delete old menu message if exists. Return True if sent.
|
"""Send menu message to the channel and delete old menu message if exists. Return True if sent.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ctx (ApplicationContext | Interaction): Context.
|
ctx (ApplicationContext | Interaction): Context.
|
||||||
|
disable (bool, optional): Disable menu message. Defaults to False.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if sent, False if not.
|
bool: True if sent, False if not.
|
||||||
"""
|
"""
|
||||||
logging.info("[VC_EXT] Sending menu message")
|
logging.info(f"[VC_EXT] Sending menu message to channel {ctx.channel_id} in guild {ctx.guild_id}")
|
||||||
|
|
||||||
if not ctx.guild_id:
|
if not ctx.guild_id:
|
||||||
logging.warning("[VC_EXT] Guild id not found in context inside 'create_menu'")
|
logging.warning("[VC_EXT] Guild id not found in context inside 'create_menu'")
|
||||||
@@ -42,14 +42,16 @@ class VoiceExtension:
|
|||||||
guild = await self.db.get_guild(ctx.guild_id, projection={'current_track': 1, 'current_menu': 1, 'vibing': 1})
|
guild = await self.db.get_guild(ctx.guild_id, projection={'current_track': 1, 'current_menu': 1, 'vibing': 1})
|
||||||
|
|
||||||
if guild['current_track']:
|
if guild['current_track']:
|
||||||
|
if not (vc := await self.get_voice_client(ctx)):
|
||||||
|
return False
|
||||||
|
|
||||||
track = cast(Track, Track.de_json(
|
track = cast(Track, Track.de_json(
|
||||||
guild['current_track'],
|
guild['current_track'],
|
||||||
client=YMClient() # type: ignore
|
client=YMClient() # type: ignore
|
||||||
))
|
))
|
||||||
embed = await generate_item_embed(track, guild['vibing'])
|
embed = await generate_item_embed(track, guild['vibing'])
|
||||||
|
|
||||||
vc = await self.get_voice_client(ctx)
|
if vc.is_paused():
|
||||||
if vc and vc.is_paused():
|
|
||||||
embed.set_footer(text='Приостановлено')
|
embed.set_footer(text='Приостановлено')
|
||||||
else:
|
else:
|
||||||
embed.remove_footer()
|
embed.remove_footer()
|
||||||
@@ -62,7 +64,7 @@ class VoiceExtension:
|
|||||||
if message:
|
if message:
|
||||||
await message.delete()
|
await message.delete()
|
||||||
|
|
||||||
await self._update_menu_views_dict(ctx)
|
await self._update_menu_views_dict(ctx, disable=disable)
|
||||||
interaction = await ctx.respond(view=menu_views[ctx.guild_id], embed=embed)
|
interaction = await ctx.respond(view=menu_views[ctx.guild_id], embed=embed)
|
||||||
response = await interaction.original_response() if isinstance(interaction, discord.Interaction) else interaction
|
response = await interaction.original_response() if isinstance(interaction, discord.Interaction) else interaction
|
||||||
await self.db.update(ctx.guild_id, {'current_menu': response.id})
|
await self.db.update(ctx.guild_id, {'current_menu': response.id})
|
||||||
@@ -81,7 +83,7 @@ class VoiceExtension:
|
|||||||
Returns:
|
Returns:
|
||||||
(discord.Message | None): Menu message or None.
|
(discord.Message | None): Menu message or None.
|
||||||
"""
|
"""
|
||||||
logging.debug(f"[VC_EXT] Fetching menu message {menu_mid}...")
|
logging.debug(f"[VC_EXT] Fetching menu message {menu_mid} in guild {ctx.guild_id}")
|
||||||
|
|
||||||
if not ctx.guild_id:
|
if not ctx.guild_id:
|
||||||
logging.warning("[VC_EXT] Guild ID not found in context")
|
logging.warning("[VC_EXT] Guild ID not found in context")
|
||||||
@@ -104,9 +106,9 @@ class VoiceExtension:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
if menu:
|
if menu:
|
||||||
logging.debug("[VC_EXT] Menu message found")
|
logging.debug(f"[VC_EXT] Menu message {menu_mid} successfully fetched")
|
||||||
else:
|
else:
|
||||||
logging.debug("[VC_EXT] Menu message not found. Resetting current_menu field.")
|
logging.debug(f"[VC_EXT] Menu message {menu_mid} not found in guild {ctx.guild_id}")
|
||||||
await self.db.update(ctx.guild_id, {'current_menu': None})
|
await self.db.update(ctx.guild_id, {'current_menu': None})
|
||||||
|
|
||||||
return menu
|
return menu
|
||||||
@@ -151,11 +153,9 @@ class VoiceExtension:
|
|||||||
if not menu_mid:
|
if not menu_mid:
|
||||||
logging.warning("[VC_EXT] No menu message or menu message id provided")
|
logging.warning("[VC_EXT] No menu message or menu message id provided")
|
||||||
return False
|
return False
|
||||||
menu = await self.get_menu_message(ctx, menu_mid)
|
menu_message = await self.get_menu_message(ctx, menu_mid)
|
||||||
else:
|
|
||||||
menu = menu_message
|
|
||||||
|
|
||||||
if not menu:
|
if not menu_message:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not guild['current_track']:
|
if not guild['current_track']:
|
||||||
@@ -164,9 +164,8 @@ class VoiceExtension:
|
|||||||
|
|
||||||
track = cast(Track, Track.de_json(
|
track = cast(Track, Track.de_json(
|
||||||
guild['current_track'],
|
guild['current_track'],
|
||||||
client=YMClient(), # type: ignore
|
client=YMClient() # type: ignore
|
||||||
))
|
))
|
||||||
|
|
||||||
embed = await generate_item_embed(track, guild['vibing'])
|
embed = await generate_item_embed(track, guild['vibing'])
|
||||||
|
|
||||||
await self._update_menu_views_dict(ctx)
|
await self._update_menu_views_dict(ctx)
|
||||||
@@ -176,12 +175,12 @@ class VoiceExtension:
|
|||||||
await ctx.edit(embed=embed, view=menu_views[gid])
|
await ctx.edit(embed=embed, view=menu_views[gid])
|
||||||
else:
|
else:
|
||||||
# If interaction from other buttons or commands. They should have their own response.
|
# If interaction from other buttons or commands. They should have their own response.
|
||||||
await menu.edit(embed=embed, view=menu_views[gid])
|
await menu_message.edit(embed=embed, view=menu_views[gid])
|
||||||
except discord.NotFound:
|
except discord.NotFound:
|
||||||
logging.warning("[VC_EXT] Menu message not found")
|
logging.warning("[VC_EXT] Menu message not found")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
logging.debug("[VC_EXT] Menu embed updated")
|
logging.debug("[VC_EXT] Menu embed updated successfully")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def update_menu_view(
|
async def update_menu_view(
|
||||||
@@ -225,12 +224,14 @@ class VoiceExtension:
|
|||||||
except discord.NotFound:
|
except discord.NotFound:
|
||||||
logging.warning("[VC_EXT] Menu message not found")
|
logging.warning("[VC_EXT] Menu message not found")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
logging.debug("[VC_EXT] Menu view updated successfully")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def update_vibe(
|
async def update_vibe(
|
||||||
self,
|
self,
|
||||||
ctx: ApplicationContext | Interaction,
|
ctx: ApplicationContext | Interaction,
|
||||||
type: Literal['track', 'album', 'artist', 'playlist', 'user'],
|
type: str,
|
||||||
id: str | int,
|
id: str | int,
|
||||||
*,
|
*,
|
||||||
update_settings: bool = False
|
update_settings: bool = False
|
||||||
@@ -240,15 +241,15 @@ class VoiceExtension:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
ctx (ApplicationContext | Interaction): Context.
|
ctx (ApplicationContext | Interaction): Context.
|
||||||
type (Literal['track', 'album', 'artist', 'playlist', 'user']): Type of the item.
|
type (str): Type of the item.
|
||||||
id (str | int): ID of the YandexMusic item.
|
id (str | int): ID of the item.
|
||||||
update_settings (bool, optional): Update vibe settings by sending feedack usind data from database. Defaults to False.
|
update_settings (bool, optional): Update vibe settings by sending feedack usind data from database. Defaults to False.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if vibe was updated successfully. False otherwise.
|
bool: True if vibe was updated successfully. False otherwise.
|
||||||
"""
|
"""
|
||||||
logging.info(f"[VC_EXT] Updating vibe for guild {ctx.guild_id} with type '{type}' and id '{id}'")
|
logging.info(f"[VC_EXT] Updating vibe for guild {ctx.guild_id} with type '{type}' and id '{id}'")
|
||||||
|
|
||||||
gid = ctx.guild_id
|
gid = ctx.guild_id
|
||||||
uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None
|
uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None
|
||||||
|
|
||||||
@@ -275,12 +276,9 @@ class VoiceExtension:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not guild['vibing']:
|
if not guild['vibing']:
|
||||||
logging.debug(f"[VIBE] Starting radio '{type}:{id}'")
|
|
||||||
|
|
||||||
feedback = await client.rotor_station_feedback_radio_started(
|
feedback = await client.rotor_station_feedback_radio_started(
|
||||||
f"{type}:{id}",
|
f"{type}:{id}",
|
||||||
f"desktop-user-{client.me.account.uid}", # type: ignore
|
f"desktop-user-{client.me.account.uid}", # type: ignore # That's made up, but it doesn't do much anyway.
|
||||||
timestamp=time()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not feedback:
|
if not feedback:
|
||||||
@@ -430,14 +428,15 @@ class VoiceExtension:
|
|||||||
logging.warning("Guild ID or User ID not found in context")
|
logging.warning("Guild ID or User ID not found in context")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
guild = await self.db.get_guild(gid, projection={'current_menu': 1, 'vibing': 1})
|
guild = await self.db.get_guild(gid, projection={'current_menu': 1, 'vibing': 1, 'current_track': 1})
|
||||||
vc = await self.get_voice_client(ctx) if not vc else vc
|
vc = await self.get_voice_client(ctx) if not vc else vc
|
||||||
|
|
||||||
if not vc:
|
if not vc:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self._download_track(gid, track)
|
if not guild['current_track'] or track.id != guild['current_track']['id']:
|
||||||
|
await self._download_track(gid, track)
|
||||||
except yandex_music.exceptions.TimedOutError:
|
except yandex_music.exceptions.TimedOutError:
|
||||||
logging.warning(f"[VC_EXT] Timed out while downloading track '{track.title}'")
|
logging.warning(f"[VC_EXT] Timed out while downloading track '{track.title}'")
|
||||||
|
|
||||||
@@ -457,6 +456,7 @@ class VoiceExtension:
|
|||||||
await self.db.set_current_track(gid, track)
|
await self.db.set_current_track(gid, track)
|
||||||
|
|
||||||
if menu_message or guild['current_menu']:
|
if menu_message or guild['current_menu']:
|
||||||
|
# Updating menu message before playing to prevent delay and avoid FFMPEG lags.
|
||||||
await self.update_menu_full(ctx, guild['current_menu'], menu_message=menu_message, button_callback=button_callback)
|
await self.update_menu_full(ctx, guild['current_menu'], menu_message=menu_message, button_callback=button_callback)
|
||||||
|
|
||||||
if not guild['vibing']:
|
if not guild['vibing']:
|
||||||
@@ -503,9 +503,11 @@ class VoiceExtension:
|
|||||||
user = await self.users_db.get_user(uid, projection={'vibe_type': 1, 'vibe_id': 1, 'vibe_batch_id': 1, 'ym_token': 1})
|
user = await self.users_db.get_user(uid, projection={'vibe_type': 1, 'vibe_id': 1, 'vibe_batch_id': 1, 'ym_token': 1})
|
||||||
vc = await self.get_voice_client(ctx) if not vc else vc
|
vc = await self.get_voice_client(ctx) if not vc else vc
|
||||||
|
|
||||||
if vc:
|
if not vc:
|
||||||
await self.db.update(gid, {'current_track': None, 'is_stopped': True})
|
return False
|
||||||
vc.stop()
|
|
||||||
|
await self.db.update(gid, {'current_track': None, 'is_stopped': True})
|
||||||
|
vc.stop()
|
||||||
|
|
||||||
if full:
|
if full:
|
||||||
if not await self._full_stop(ctx, guild, gid):
|
if not await self._full_stop(ctx, guild, gid):
|
||||||
@@ -580,10 +582,10 @@ class VoiceExtension:
|
|||||||
logging.debug("[VC_EXT] Repeating current track")
|
logging.debug("[VC_EXT] Repeating current track")
|
||||||
next_track = guild['current_track']
|
next_track = guild['current_track']
|
||||||
elif guild['shuffle']:
|
elif guild['shuffle']:
|
||||||
logging.debug("[VC_EXT] Shuffling tracks")
|
logging.debug("[VC_EXT] Getting random track from queue")
|
||||||
next_track = await self.db.pop_random_track(gid, 'next')
|
next_track = await self.db.pop_random_track(gid, 'next')
|
||||||
else:
|
else:
|
||||||
logging.debug("[VC_EXT] Getting next track")
|
logging.debug("[VC_EXT] Getting next track from queue")
|
||||||
next_track = await self.db.get_track(gid, 'next')
|
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'] and not isinstance(ctx, discord.RawReactionActionEvent):
|
||||||
@@ -854,8 +856,7 @@ class VoiceExtension:
|
|||||||
feedback = await client.rotor_station_feedback_track_started(
|
feedback = await client.rotor_station_feedback_track_started(
|
||||||
f"{user['vibe_type']}:{user['vibe_id']}",
|
f"{user['vibe_type']}:{user['vibe_id']}",
|
||||||
track.id,
|
track.id,
|
||||||
user['vibe_batch_id'], # type: ignore # wrong typehints
|
user['vibe_batch_id'] # type: ignore # Wrong typehints
|
||||||
time()
|
|
||||||
)
|
)
|
||||||
logging.debug(f"[VIBE] Track started feedback: {feedback}")
|
logging.debug(f"[VIBE] Track started feedback: {feedback}")
|
||||||
return True
|
return True
|
||||||
@@ -898,8 +899,7 @@ class VoiceExtension:
|
|||||||
f"{user['vibe_type']}:{user['vibe_id']}",
|
f"{user['vibe_type']}:{user['vibe_id']}",
|
||||||
track['id'],
|
track['id'],
|
||||||
track['duration_ms'] // 1000,
|
track['duration_ms'] // 1000,
|
||||||
cast(str, user['vibe_batch_id']),
|
user['vibe_batch_id'] # type: ignore # Wrong typehints
|
||||||
time()
|
|
||||||
)
|
)
|
||||||
logging.info(f"[VOICE] User {user['_id']} finished vibing with result: {res}")
|
logging.info(f"[VOICE] User {user['_id']} finished vibing with result: {res}")
|
||||||
return True
|
return True
|
||||||
@@ -941,23 +941,21 @@ class VoiceExtension:
|
|||||||
f'{user['vibe_type']}:{user['vibe_id']}',
|
f'{user['vibe_type']}:{user['vibe_id']}',
|
||||||
guild['current_track']['id'],
|
guild['current_track']['id'],
|
||||||
guild['current_track']['duration_ms'] // 1000,
|
guild['current_track']['duration_ms'] // 1000,
|
||||||
user['vibe_batch_id'], # type: ignore # Wrong typehints
|
user['vibe_batch_id'] # type: ignore # Wrong typehints
|
||||||
time()
|
|
||||||
)
|
)
|
||||||
logging.debug(f"[VIBE] Finished track: {feedback}")
|
logging.debug(f"[VIBE] Finished track feeedback: {feedback}")
|
||||||
else:
|
else:
|
||||||
feedback = await client.rotor_station_feedback_skip(
|
feedback = await client.rotor_station_feedback_skip(
|
||||||
f'{user['vibe_type']}:{user['vibe_id']}',
|
f'{user['vibe_type']}:{user['vibe_id']}',
|
||||||
guild['current_track']['id'],
|
guild['current_track']['id'],
|
||||||
guild['current_track']['duration_ms'] // 1000,
|
guild['current_track']['duration_ms'] // 1000,
|
||||||
user['vibe_batch_id'], # type: ignore # Wrong typehints
|
user['vibe_batch_id'] # type: ignore # Wrong typehints
|
||||||
time()
|
|
||||||
)
|
)
|
||||||
if not feedback:
|
if not feedback:
|
||||||
logging.warning("[VIBE] Failed to send vibe feedback")
|
logging.warning("[VIBE] Failed to send vibe feedback")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
logging.debug(f"[VIBE] Skipped track: {feedback}")
|
logging.debug(f"[VIBE] Skipped track feeedback: {feedback}")
|
||||||
feedback = await self.update_vibe(
|
feedback = await self.update_vibe(
|
||||||
ctx,
|
ctx,
|
||||||
user['vibe_type'],
|
user['vibe_type'],
|
||||||
@@ -988,18 +986,21 @@ class VoiceExtension:
|
|||||||
Returns:
|
Returns:
|
||||||
str | None: Song title or None.
|
str | None: Song title or None.
|
||||||
"""
|
"""
|
||||||
logging.debug("[VC_EXT] Playing next track")
|
|
||||||
|
|
||||||
client = await self.init_ym_client(ctx) if not client else client
|
client = await self.init_ym_client(ctx) if not client else client
|
||||||
|
|
||||||
if not client:
|
if not client:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if not vc:
|
||||||
|
vc = await self.get_voice_client(ctx)
|
||||||
|
|
||||||
|
if not await self.stop_playing(ctx, vc=vc):
|
||||||
|
return None
|
||||||
|
|
||||||
ym_track = cast(Track, Track.de_json(
|
ym_track = cast(Track, Track.de_json(
|
||||||
next_track,
|
next_track,
|
||||||
client=client # type: ignore # Async client can be used here.
|
client=client # type: ignore # Async client can be used here.
|
||||||
))
|
))
|
||||||
await self.stop_playing(ctx, vc=vc)
|
|
||||||
return await self.play_track(
|
return await self.play_track(
|
||||||
ctx,
|
ctx,
|
||||||
ym_track,
|
ym_track,
|
||||||
|
|||||||
@@ -4,12 +4,37 @@ from typing import cast
|
|||||||
import discord
|
import discord
|
||||||
from discord.ext.commands import Cog
|
from discord.ext.commands import Cog
|
||||||
|
|
||||||
|
from yandex_music import ClientAsync as YMClient
|
||||||
|
from yandex_music.exceptions import UnauthorizedError
|
||||||
|
|
||||||
|
from MusicBot.database import BaseUsersDatabase
|
||||||
from MusicBot.cogs.utils import VoiceExtension, menu_views
|
from MusicBot.cogs.utils import VoiceExtension, menu_views
|
||||||
from MusicBot.ui import QueueView, generate_queue_embed
|
from MusicBot.ui import QueueView, generate_queue_embed
|
||||||
|
|
||||||
def setup(bot: discord.Bot):
|
def setup(bot: discord.Bot):
|
||||||
bot.add_cog(Voice(bot))
|
bot.add_cog(Voice(bot))
|
||||||
|
|
||||||
|
users_db = BaseUsersDatabase()
|
||||||
|
|
||||||
|
async def get_vibe_stations_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)
|
||||||
|
if not token:
|
||||||
|
logging.info(f"[GENERAL] User {ctx.interaction.user.id} has no token")
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = await YMClient(token).init()
|
||||||
|
except UnauthorizedError:
|
||||||
|
logging.info(f"[GENERAL] User {ctx.interaction.user.id} provided invalid token")
|
||||||
|
return []
|
||||||
|
|
||||||
|
stations = await client.rotor_stations_list()
|
||||||
|
return [station.station.name for station in stations if station.station and ctx.value in station.station.name][:100]
|
||||||
|
|
||||||
|
|
||||||
class Voice(Cog, VoiceExtension):
|
class Voice(Cog, VoiceExtension):
|
||||||
|
|
||||||
voice = discord.SlashCommandGroup("voice", "Команды, связанные с голосовым каналом.")
|
voice = discord.SlashCommandGroup("voice", "Команды, связанные с голосовым каналом.")
|
||||||
@@ -257,14 +282,17 @@ class Voice(Cog, VoiceExtension):
|
|||||||
await ctx.respond("❌ У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True)
|
await ctx.respond("❌ У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
if (vc := await self.get_voice_client(ctx)) and await self.voice_check(ctx):
|
if (vc := await self.get_voice_client(ctx)) and await self.voice_check(ctx) and vc.is_connected:
|
||||||
res = await self.stop_playing(ctx, full=True)
|
res = await self.stop_playing(ctx, full=True)
|
||||||
if res:
|
if res:
|
||||||
await vc.disconnect(force=True)
|
await vc.disconnect(force=True)
|
||||||
await ctx.respond("Отключение успешно!", delete_after=15, ephemeral=True)
|
await ctx.respond("Отключение успешно!", delete_after=15, ephemeral=True)
|
||||||
logging.info(f"[VOICE] Successfully disconnected from voice channel in guild {ctx.guild.id}")
|
logging.info(f"[VOICE] Successfully disconnected from voice channel in guild {ctx.guild.id}")
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
await ctx.respond("❌ Не удалось отключиться.", delete_after=15, ephemeral=True)
|
await ctx.respond("❌ Не удалось отключиться.", delete_after=15, ephemeral=True)
|
||||||
|
else:
|
||||||
|
await ctx.respond("❌ Бот не подключен к голосовому каналу.", delete_after=15, ephemeral=True)
|
||||||
|
|
||||||
@queue.command(description="Очистить очередь треков и историю прослушивания.")
|
@queue.command(description="Очистить очередь треков и историю прослушивания.")
|
||||||
async def clear(self, ctx: discord.ApplicationContext) -> None:
|
async def clear(self, ctx: discord.ApplicationContext) -> None:
|
||||||
@@ -436,13 +464,19 @@ class Voice(Cog, VoiceExtension):
|
|||||||
if not await self.voice_check(ctx):
|
if not await self.voice_check(ctx):
|
||||||
return
|
return
|
||||||
|
|
||||||
guild = await self.db.get_guild(ctx.guild.id, projection={'always_allow_menu': 1, 'current_track': 1, 'current_menu': 1})
|
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)
|
channel = cast(discord.VoiceChannel, ctx.channel)
|
||||||
|
|
||||||
if len(channel.members) > 2 and not guild['always_allow_menu']:
|
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")
|
logging.info(f"[VOICE] Action declined: other members are present in the voice channel")
|
||||||
await ctx.respond("❌ Вы не единственный в голосовом канале.", ephemeral=True)
|
await ctx.respond("❌ Вы не единственный в голосовом канале.", ephemeral=True)
|
||||||
return
|
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']:
|
if not guild['current_track']:
|
||||||
logging.info(f"[VOICE] No current track in {ctx.guild.id}")
|
logging.info(f"[VOICE] No current track in {ctx.guild.id}")
|
||||||
await ctx.respond("❌ Нет воспроизводимого трека.", ephemeral=True)
|
await ctx.respond("❌ Нет воспроизводимого трека.", ephemeral=True)
|
||||||
@@ -452,35 +486,81 @@ class Voice(Cog, VoiceExtension):
|
|||||||
if not feedback:
|
if not feedback:
|
||||||
await ctx.respond("❌ Операция не удалась. Возможно, у вес нет подписки на Яндекс Музыку.", ephemeral=True)
|
await ctx.respond("❌ Операция не удалась. Возможно, у вес нет подписки на Яндекс Музыку.", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not guild['current_menu']:
|
if not guild['current_menu']:
|
||||||
await self.send_menu_message(ctx)
|
await self.send_menu_message(ctx, disable=True)
|
||||||
|
|
||||||
next_track = await self.db.get_track(ctx.guild_id, 'next')
|
next_track = await self.db.get_track(ctx.guild_id, 'next')
|
||||||
if next_track:
|
if next_track:
|
||||||
await self._play_next_track(ctx, next_track)
|
await self._play_next_track(ctx, next_track)
|
||||||
|
|
||||||
@voice.command(name='vibe', description="Запустить Мою Волну.")
|
@voice.command(name='vibe', description="Запустить Мою Волну.")
|
||||||
async def user_vibe(self, ctx: discord.ApplicationContext) -> None:
|
@discord.option(
|
||||||
|
"запрос",
|
||||||
|
parameter_name='name',
|
||||||
|
description="Название станции.",
|
||||||
|
type=discord.SlashCommandOptionType.string,
|
||||||
|
autocomplete=discord.utils.basic_autocomplete(get_vibe_stations_suggestions),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
async def user_vibe(self, ctx: discord.ApplicationContext, name: str | None = None) -> None:
|
||||||
logging.info(f"[VOICE] Vibe (user) command invoked by user {ctx.user.id} in guild {ctx.guild_id}")
|
logging.info(f"[VOICE] Vibe (user) command invoked by user {ctx.user.id} in guild {ctx.guild_id}")
|
||||||
if not await self.voice_check(ctx):
|
if not await self.voice_check(ctx):
|
||||||
return
|
return
|
||||||
|
|
||||||
guild = await self.db.get_guild(ctx.guild.id, projection={'always_allow_menu': 1, 'current_menu': 1})
|
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)
|
channel = cast(discord.VoiceChannel, ctx.channel)
|
||||||
|
|
||||||
if len(channel.members) > 2 and not guild['always_allow_menu']:
|
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")
|
logging.info(f"[VOICE] Action declined: other members are present in the voice channel")
|
||||||
await ctx.respond("❌ Вы не единственный в голосовом канале.", ephemeral=True)
|
await ctx.respond("❌ Вы не единственный в голосовом канале.", ephemeral=True)
|
||||||
return
|
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 name:
|
||||||
|
token = await users_db.get_ym_token(ctx.user.id)
|
||||||
|
if not token:
|
||||||
|
logging.info(f"[GENERAL] User {ctx.user.id} has no token")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = await YMClient(token).init()
|
||||||
|
except UnauthorizedError:
|
||||||
|
logging.info(f"[GENERAL] User {ctx.user.id} provided invalid token")
|
||||||
|
return
|
||||||
|
|
||||||
|
stations = await client.rotor_stations_list()
|
||||||
|
for content in stations:
|
||||||
|
if content.station and content.station.name == name and content.ad_params:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
content = None
|
||||||
|
|
||||||
|
if not content:
|
||||||
|
logging.debug(f"[VOICE] Station {name} not found")
|
||||||
|
await ctx.respond("❌ Станция не найдена.", 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)
|
||||||
|
return
|
||||||
|
|
||||||
|
feedback = await self.update_vibe(ctx, _type, _id)
|
||||||
|
else:
|
||||||
|
feedback = await self.update_vibe(ctx, 'user', 'onyourwave')
|
||||||
|
|
||||||
feedback = await self.update_vibe(ctx, 'user', 'onyourwave')
|
|
||||||
if not feedback:
|
if not feedback:
|
||||||
await ctx.respond("❌ Операция не удалась. Возможно, у вес нет подписки на Яндекс Музыку.", ephemeral=True)
|
await ctx.respond("❌ Операция не удалась. Возможно, у вес нет подписки на Яндекс Музыку.", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not guild['current_menu']:
|
if not guild['current_menu']:
|
||||||
await self.send_menu_message(ctx)
|
await self.send_menu_message(ctx, disable=True)
|
||||||
|
|
||||||
next_track = await self.db.get_track(ctx.guild_id, 'next')
|
next_track = await self.db.get_track(ctx.guild_id, 'next')
|
||||||
if next_track:
|
if next_track:
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ class BaseGuildsDatabase:
|
|||||||
is_stopped=True,
|
is_stopped=True,
|
||||||
allow_explicit=True,
|
allow_explicit=True,
|
||||||
always_allow_menu=False,
|
always_allow_menu=False,
|
||||||
allow_connect=False,
|
allow_connect=True,
|
||||||
vote_next_track=True,
|
vote_next_track=True,
|
||||||
vote_add_track=True,
|
vote_add_track=True,
|
||||||
vote_add_album=True,
|
vote_add_album=True,
|
||||||
|
|||||||
@@ -17,17 +17,18 @@ cogs_list = [
|
|||||||
@bot.event
|
@bot.event
|
||||||
async def on_ready():
|
async def on_ready():
|
||||||
logging.info("Bot's ready!")
|
logging.info("Bot's ready!")
|
||||||
|
await bot.change_presence(activity=discord.Activity(type=discord.ActivityType.listening, name="/voice vibe"))
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import coloredlogs
|
import coloredlogs
|
||||||
coloredlogs.install(level=logging.DEBUG)
|
coloredlogs.install(level=logging.DEBUG)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if os.getenv('DEBUG') == 'True':
|
if os.getenv('DEBUG') == 'True':
|
||||||
logging.getLogger().setLevel(logging.DEBUG)
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
logging.getLogger('discord').setLevel(logging.INFO)
|
logging.getLogger('discord').setLevel(logging.INFO)
|
||||||
@@ -38,14 +39,14 @@ if __name__ == '__main__':
|
|||||||
logging.getLogger('discord').setLevel(logging.WARNING)
|
logging.getLogger('discord').setLevel(logging.WARNING)
|
||||||
logging.getLogger('pymongo').setLevel(logging.WARNING)
|
logging.getLogger('pymongo').setLevel(logging.WARNING)
|
||||||
logging.getLogger('yandex_music').setLevel(logging.WARNING)
|
logging.getLogger('yandex_music').setLevel(logging.WARNING)
|
||||||
|
|
||||||
if not os.path.exists('music'):
|
if not os.path.exists('music'):
|
||||||
os.mkdir('music')
|
os.mkdir('music')
|
||||||
token = os.getenv('TOKEN')
|
token = os.getenv('TOKEN')
|
||||||
if not token:
|
if not token:
|
||||||
raise ValueError('You must specify the bot TOKEN in your enviroment')
|
raise ValueError('You must specify the bot TOKEN in your enviroment')
|
||||||
|
|
||||||
for cog in cogs_list:
|
for cog in cogs_list:
|
||||||
bot.load_extension(f'MusicBot.cogs.{cog}')
|
bot.load_extension(f'MusicBot.cogs.{cog}')
|
||||||
|
|
||||||
bot.run(token)
|
bot.run(token)
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
from .other import MyPlaylists, QueueView, generate_queue_embed, generate_playlists_embed
|
from .other import QueueView, generate_queue_embed
|
||||||
from .menu import MenuView
|
from .menu import MenuView
|
||||||
from .find import ListenView
|
from .find import ListenView
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'MyPlaylists',
|
|
||||||
'QueueView',
|
'QueueView',
|
||||||
'MenuView',
|
'MenuView',
|
||||||
'ListenView',
|
'ListenView',
|
||||||
'generate_queue_embed',
|
'generate_queue_embed'
|
||||||
'generate_playlists_embed'
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -153,17 +153,33 @@ class MyVibeButton(Button, VoiceExtension):
|
|||||||
await interaction.respond("❌ Вы не единственный в голосовом канале.", ephemeral=True)
|
await interaction.respond("❌ Вы не единственный в голосовом канале.", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
track_type_map: dict[Any, Literal['track', 'album', 'artist', 'playlist', 'user']] = {
|
track_type_map = {
|
||||||
Track: 'track', Album: 'album', Artist: 'artist', Playlist: 'playlist', list: 'user'
|
Track: 'track', Album: 'album', Artist: 'artist', Playlist: 'playlist', list: 'user'
|
||||||
} # NOTE: Likes playlist should have its own entry instead of 'user:onyourwave'
|
}
|
||||||
|
|
||||||
await self.send_menu_message(interaction)
|
if isinstance(self.item, Playlist):
|
||||||
|
if not self.item.owner:
|
||||||
|
logging.warning(f"[VIBE] Playlist owner is None")
|
||||||
|
await interaction.respond("❌ Не удалось получить информацию о плейлисте.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
_id = self.item.owner.login + '_' + str(self.item.kind)
|
||||||
|
elif not isinstance(self.item, list):
|
||||||
|
_id = cast(int | str, self.item.id)
|
||||||
|
else:
|
||||||
|
_id = 'onyourwave'
|
||||||
|
|
||||||
|
await self.send_menu_message(interaction, disable=True)
|
||||||
await self.update_vibe(
|
await self.update_vibe(
|
||||||
interaction,
|
interaction,
|
||||||
track_type_map[type(self.item)],
|
track_type_map[type(self.item)],
|
||||||
cast(int, self.item.uid) if isinstance(self.item, Playlist) else cast(int | str, self.item.id) if not isinstance(self.item, list) else 'onyourwave'
|
_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
next_track = await self.db.get_track(gid, 'next')
|
||||||
|
if next_track:
|
||||||
|
await self._play_next_track(interaction, next_track)
|
||||||
|
|
||||||
class ListenView(View):
|
class ListenView(View):
|
||||||
def __init__(self, item: Track | Album | Artist | Playlist | list[Track], *items: Item, timeout: float | None = 360, disable_on_timeout: bool = True):
|
def __init__(self, item: Track | Album | Artist | Playlist | list[Track], *items: Item, timeout: float | None = 360, disable_on_timeout: bool = True):
|
||||||
super().__init__(*items, timeout=timeout, disable_on_timeout=disable_on_timeout)
|
super().__init__(*items, timeout=timeout, disable_on_timeout=disable_on_timeout)
|
||||||
@@ -195,7 +211,7 @@ class ListenView(View):
|
|||||||
self.add_item(self.button2)
|
self.add_item(self.button2)
|
||||||
self.add_item(self.button3)
|
self.add_item(self.button3)
|
||||||
self.add_item(self.button4)
|
self.add_item(self.button4)
|
||||||
|
|
||||||
async def on_timeout(self) -> None:
|
async def on_timeout(self) -> None:
|
||||||
try:
|
try:
|
||||||
return await super().on_timeout()
|
return await super().on_timeout()
|
||||||
|
|||||||
@@ -239,11 +239,11 @@ class MyVibeSelect(Select, VoiceExtension):
|
|||||||
await interaction.edit(view=view)
|
await interaction.edit(view=view)
|
||||||
|
|
||||||
class MyVibeSettingsView(View, VoiceExtension):
|
class MyVibeSettingsView(View, VoiceExtension):
|
||||||
def __init__(self, interaction: Interaction, *items: Item, timeout: float | None = 360, disable_on_timeout: bool = True):
|
def __init__(self, interaction: Interaction, *items: Item, timeout: float | None = None, disable_on_timeout: bool = True):
|
||||||
View.__init__(self, *items, timeout=timeout, disable_on_timeout=disable_on_timeout)
|
View.__init__(self, *items, timeout=timeout, disable_on_timeout=disable_on_timeout)
|
||||||
VoiceExtension.__init__(self, None)
|
VoiceExtension.__init__(self, None)
|
||||||
self.interaction = interaction
|
self.interaction = interaction
|
||||||
|
|
||||||
async def init(self) -> Self:
|
async def init(self) -> Self:
|
||||||
if not self.interaction.user:
|
if not self.interaction.user:
|
||||||
logging.warning('[MENU] No user in settings view')
|
logging.warning('[MENU] No user in settings view')
|
||||||
|
|||||||
@@ -6,19 +6,6 @@ from discord import ApplicationContext, ButtonStyle, Interaction, Embed
|
|||||||
|
|
||||||
from MusicBot.cogs.utils.voice_extension import VoiceExtension
|
from MusicBot.cogs.utils.voice_extension import VoiceExtension
|
||||||
|
|
||||||
def generate_playlists_embed(page: int, playlists: list[tuple[str, int]]) -> Embed:
|
|
||||||
count = 15 * page
|
|
||||||
length = len(playlists)
|
|
||||||
embed = Embed(
|
|
||||||
title=f"Всего плейлистов: {length}",
|
|
||||||
color=0xfed42b
|
|
||||||
)
|
|
||||||
embed.set_author(name="Ваши плейлисты")
|
|
||||||
embed.set_footer(text=f"Страница {page + 1} из {ceil(length / 10)}")
|
|
||||||
for playlist in playlists[count:count + 10]:
|
|
||||||
embed.add_field(name=playlist[0], value=f"{playlist[1]} треков", inline=False)
|
|
||||||
return embed
|
|
||||||
|
|
||||||
def generate_queue_embed(page: int, tracks_list: list[dict[str, Any]]) -> Embed:
|
def generate_queue_embed(page: int, tracks_list: list[dict[str, Any]]) -> Embed:
|
||||||
count = 15 * page
|
count = 15 * page
|
||||||
length = len(tracks_list)
|
length = len(tracks_list)
|
||||||
@@ -36,61 +23,6 @@ def generate_queue_embed(page: int, tracks_list: list[dict[str, Any]]) -> Embed:
|
|||||||
embed.add_field(name=f"{i} - {track['title']} - {duration_m}:{duration_s:02d}", value="", inline=False)
|
embed.add_field(name=f"{i} - {track['title']} - {duration_m}:{duration_s:02d}", value="", inline=False)
|
||||||
return embed
|
return embed
|
||||||
|
|
||||||
|
|
||||||
class MPNextButton(Button, VoiceExtension):
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
Button.__init__(self, **kwargs)
|
|
||||||
VoiceExtension.__init__(self, None)
|
|
||||||
|
|
||||||
async def callback(self, interaction: Interaction) -> None:
|
|
||||||
if not interaction.user:
|
|
||||||
return
|
|
||||||
user = await self.users_db.get_user(interaction.user.id)
|
|
||||||
page = user['playlists_page'] + 1
|
|
||||||
await self.users_db.update(interaction.user.id, {'playlists_page': page})
|
|
||||||
embed = generate_playlists_embed(page, user['playlists'])
|
|
||||||
await interaction.edit(embed=embed, view=await MyPlaylists(interaction).init())
|
|
||||||
|
|
||||||
class MPPrevButton(Button, VoiceExtension):
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
Button.__init__(self, **kwargs)
|
|
||||||
VoiceExtension.__init__(self, None)
|
|
||||||
|
|
||||||
async def callback(self, interaction: Interaction) -> None:
|
|
||||||
if not interaction.user:
|
|
||||||
return
|
|
||||||
user = await self.users_db.get_user(interaction.user.id)
|
|
||||||
page = user['playlists_page'] - 1
|
|
||||||
await self.users_db.update(interaction.user.id, {'playlists_page': page})
|
|
||||||
embed = generate_playlists_embed(page, user['playlists'])
|
|
||||||
await interaction.edit(embed=embed, view=await MyPlaylists(interaction).init())
|
|
||||||
|
|
||||||
class MyPlaylists(View, VoiceExtension):
|
|
||||||
def __init__(self, ctx: ApplicationContext | Interaction, *items: Item, timeout: float | None = 360, disable_on_timeout: bool = True):
|
|
||||||
View.__init__(self, *items, timeout=timeout, disable_on_timeout=disable_on_timeout)
|
|
||||||
VoiceExtension.__init__(self, None)
|
|
||||||
|
|
||||||
self.ctx = ctx
|
|
||||||
self.next_button = MPNextButton(style=ButtonStyle.primary, emoji='▶️')
|
|
||||||
self.prev_button = MPPrevButton(style=ButtonStyle.primary, emoji='◀️')
|
|
||||||
|
|
||||||
async def init(self) -> Self:
|
|
||||||
if not self.ctx.user:
|
|
||||||
return self
|
|
||||||
|
|
||||||
user = await self.users_db.get_user(self.ctx.user.id)
|
|
||||||
count = 10 * user['playlists_page']
|
|
||||||
|
|
||||||
if not user['playlists'][count + 10:]:
|
|
||||||
self.next_button.disabled = True
|
|
||||||
if not user['playlists'][:count]:
|
|
||||||
self.prev_button.disabled = True
|
|
||||||
|
|
||||||
self.add_item(self.prev_button)
|
|
||||||
self.add_item(self.next_button)
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
class QueueNextButton(Button, VoiceExtension):
|
class QueueNextButton(Button, VoiceExtension):
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
Button.__init__(self, **kwargs)
|
Button.__init__(self, **kwargs)
|
||||||
|
|||||||
Reference in New Issue
Block a user