feat: Add station search for /voice vibe.

This commit is contained in:
Lemon4ksan
2025-02-13 17:30:17 +03:00
parent c0bb10cbf8
commit 8a3f06399d
5 changed files with 104 additions and 28 deletions

View File

@@ -57,11 +57,8 @@ 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]
@@ -81,7 +78,7 @@ async def get_user_playlists_suggestions(ctx: discord.AutocompleteContext) -> li
return [] return []
playlists_list = await client.users_playlists_list() playlists_list = await client.users_playlists_list()
return [playlist.title if playlist.title else 'Без названия' for playlist in 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):
@@ -159,12 +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" "Разрешить или запретить отключение/подключение бота к каналу участникам без прав управления каналом.\n```/settings connect```\n"
"`Примечание`: Только пользователи с разрешением управления каналом могут менять настройки."
) )
elif command == 'track': elif command == 'track':
embed.description += ( embed.description += (
@@ -179,10 +176,10 @@ class General(Cog):
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 = '❌ Неизвестная команда.'
@@ -410,8 +407,8 @@ class General(Cog):
logging.info(f"[GENERAL] User {ctx.user.id} search for '{name}' returned no results") logging.info(f"[GENERAL] User {ctx.user.id} search for '{name}' returned no results")
await ctx.respond("❌ По запросу ничего не найдено.", delete_after=15, ephemeral=True) await ctx.respond("❌ По запросу ничего не найдено.", delete_after=15, ephemeral=True)
return return
content = content.results[0]
content = content.results[0]
embed = await generate_item_embed(content) embed = await generate_item_embed(content)
view = ListenView(content) view = ListenView(content)
@@ -431,6 +428,7 @@ class General(Cog):
view = None view = None
embed.set_footer(text="Воспроизведение недоступно, так как у автора присутствуют Explicit треки") embed.set_footer(text="Воспроизведение недоступно, так как у автора присутствуют Explicit треки")
break break
elif isinstance(content, Playlist): elif isinstance(content, Playlist):
tracks = await content.fetch_tracks_async() tracks = await content.fetch_tracks_async()
if not tracks: if not tracks:

View File

@@ -231,7 +231,7 @@ class VoiceExtension:
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
@@ -241,8 +241,8 @@ 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:

View File

@@ -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", "Команды, связанные с голосовым каналом.")
@@ -436,13 +461,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,7 +483,7 @@ 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, disable=True) await self.send_menu_message(ctx, disable=True)
@@ -461,24 +492,70 @@ class Voice(Cog, VoiceExtension):
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, disable=True) await self.send_menu_message(ctx, disable=True)

View File

@@ -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)

View File

@@ -153,7 +153,7 @@ 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'
} }
@@ -211,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()