mirror of
https://github.com/deadcxap/YandexMusicDiscordBot.git
synced 2026-01-11 02:01:43 +03:00
feat: Add "My Vibe" button to menu and enhance interactions.
This commit is contained in:
@@ -245,7 +245,6 @@ class General(Cog):
|
|||||||
logging.info(f"Successfully fetched playlists for user {ctx.user.id}")
|
logging.info(f"Successfully fetched playlists for user {ctx.user.id}")
|
||||||
await ctx.respond(embed=embed, view=MyPlaylists(ctx), ephemeral=True)
|
await ctx.respond(embed=embed, view=MyPlaylists(ctx), ephemeral=True)
|
||||||
|
|
||||||
discord.Option
|
|
||||||
@discord.slash_command(description="Найти контент и отправить информацию о нём. Возвращается лучшее совпадение.")
|
@discord.slash_command(description="Найти контент и отправить информацию о нём. Возвращается лучшее совпадение.")
|
||||||
@discord.option(
|
@discord.option(
|
||||||
"тип",
|
"тип",
|
||||||
|
|||||||
@@ -58,6 +58,10 @@ def _generate_likes_embed(tracks: list[Track]) -> Embed:
|
|||||||
|
|
||||||
duration_m = duration // 60000
|
duration_m = duration // 60000
|
||||||
duration_s = ceil(duration / 1000) - duration_m * 60
|
duration_s = ceil(duration / 1000) - duration_m * 60
|
||||||
|
if duration_s == 60:
|
||||||
|
duration_m += 1
|
||||||
|
duration_s = 0
|
||||||
|
|
||||||
embed.add_field(name="Длительность", value=f"{duration_m}:{duration_s:02}")
|
embed.add_field(name="Длительность", value=f"{duration_m}:{duration_s:02}")
|
||||||
|
|
||||||
if track_count is not None:
|
if track_count is not None:
|
||||||
@@ -89,6 +93,9 @@ async def _generate_track_embed(track: Track) -> Embed:
|
|||||||
|
|
||||||
duration_m = duration // 60000
|
duration_m = duration // 60000
|
||||||
duration_s = ceil(duration / 1000) - duration_m * 60
|
duration_s = ceil(duration / 1000) - duration_m * 60
|
||||||
|
if duration_s == 60:
|
||||||
|
duration_m += 1
|
||||||
|
duration_s = 0
|
||||||
|
|
||||||
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
|
||||||
@@ -184,6 +191,9 @@ async def _generate_album_embed(album: Album) -> Embed:
|
|||||||
if duration:
|
if duration:
|
||||||
duration_m = duration // 60000
|
duration_m = duration // 60000
|
||||||
duration_s = ceil(duration / 1000) - duration_m * 60
|
duration_s = ceil(duration / 1000) - duration_m * 60
|
||||||
|
if duration_s == 60:
|
||||||
|
duration_m += 1
|
||||||
|
duration_s = 0
|
||||||
embed.add_field(name="Длительность", value=f"{duration_m}:{duration_s:02}")
|
embed.add_field(name="Длительность", value=f"{duration_m}:{duration_s:02}")
|
||||||
|
|
||||||
if track_count is not None:
|
if track_count is not None:
|
||||||
@@ -291,6 +301,9 @@ async def _generate_playlist_embed(playlist: Playlist) -> Embed:
|
|||||||
if duration:
|
if duration:
|
||||||
duration_m = duration // 60000
|
duration_m = duration // 60000
|
||||||
duration_s = ceil(duration / 1000) - duration_m * 60
|
duration_s = ceil(duration / 1000) - duration_m * 60
|
||||||
|
if duration_s == 60:
|
||||||
|
duration_m += 1
|
||||||
|
duration_s = 0
|
||||||
embed.add_field(name="Длительность", value=f"{duration_m}:{duration_s:02}")
|
embed.add_field(name="Длительность", value=f"{duration_m}:{duration_s:02}")
|
||||||
|
|
||||||
if track_count is not None:
|
if track_count is not None:
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ from discord import Interaction, ApplicationContext, RawReactionActionEvent
|
|||||||
from MusicBot.cogs.utils import generate_item_embed
|
from MusicBot.cogs.utils import generate_item_embed
|
||||||
from MusicBot.database import VoiceGuildsDatabase, BaseUsersDatabase
|
from MusicBot.database import VoiceGuildsDatabase, BaseUsersDatabase
|
||||||
|
|
||||||
|
# TODO: RawReactionActionEvent is poorly supported.
|
||||||
|
|
||||||
class VoiceExtension:
|
class VoiceExtension:
|
||||||
|
|
||||||
def __init__(self, bot: discord.Bot | None) -> None:
|
def __init__(self, bot: discord.Bot | None) -> None:
|
||||||
@@ -21,23 +23,21 @@ class VoiceExtension:
|
|||||||
|
|
||||||
async def send_menu_message(self, ctx: ApplicationContext | Interaction) -> None:
|
async def send_menu_message(self, ctx: ApplicationContext | Interaction) -> None:
|
||||||
from MusicBot.ui import MenuView
|
from MusicBot.ui import MenuView
|
||||||
logging.info(f"Sending player menu")
|
logging.info("[VC] Sending player menu")
|
||||||
|
|
||||||
if not ctx.guild:
|
if not ctx.guild:
|
||||||
logging.warning("Guild not found in context inside 'create_menu'")
|
logging.warning("[VC] Guild not found in context inside 'create_menu'")
|
||||||
return
|
return
|
||||||
|
|
||||||
guild = self.db.get_guild(ctx.guild.id)
|
guild = self.db.get_guild(ctx.guild.id)
|
||||||
embed = None
|
embed = None
|
||||||
|
|
||||||
if guild['current_track']:
|
if guild['current_track']:
|
||||||
embed = await generate_item_embed(
|
track = cast(Track, Track.de_json(
|
||||||
Track.de_json(
|
guild['current_track'],
|
||||||
guild['current_track'],
|
client=YMClient() # type: ignore # Async client can be used here.
|
||||||
client=YMClient() # type: ignore # Async client can be used here.
|
))
|
||||||
),
|
embed = await generate_item_embed(track, guild['vibing'])
|
||||||
guild['vibing']
|
|
||||||
)
|
|
||||||
vc = await self.get_voice_client(ctx)
|
vc = await self.get_voice_client(ctx)
|
||||||
if vc and vc.is_paused():
|
if vc and vc.is_paused():
|
||||||
embed.set_footer(text='Приостановлено')
|
embed.set_footer(text='Приостановлено')
|
||||||
@@ -45,7 +45,7 @@ class VoiceExtension:
|
|||||||
embed.remove_footer()
|
embed.remove_footer()
|
||||||
|
|
||||||
if guild['current_menu']:
|
if guild['current_menu']:
|
||||||
logging.info(f"Deleteing old player menu {guild['current_menu']} in guild {ctx.guild.id}")
|
logging.info(f"[VC] Deleting old player menu {guild['current_menu']} in guild {ctx.guild.id}")
|
||||||
message = await self.get_menu_message(ctx, guild['current_menu'])
|
message = await self.get_menu_message(ctx, guild['current_menu'])
|
||||||
if message:
|
if message:
|
||||||
await message.delete()
|
await message.delete()
|
||||||
@@ -54,19 +54,60 @@ class VoiceExtension:
|
|||||||
response = await interaction.original_response()
|
response = await interaction.original_response()
|
||||||
self.db.update(ctx.guild.id, {'current_menu': response.id})
|
self.db.update(ctx.guild.id, {'current_menu': response.id})
|
||||||
|
|
||||||
logging.info(f"New player menu {response.id} created in guild {ctx.guild.id}")
|
logging.info(f"[VC] New player menu {response.id} created in guild {ctx.guild.id}")
|
||||||
|
|
||||||
|
async def get_menu_message(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, player_mid: int) -> discord.Message | None:
|
||||||
|
"""Fetch the player message by its id. Return the message if found, None if not.
|
||||||
|
Reset `current_menu` field in the database if not found.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ctx (ApplicationContext | Interaction): Context.
|
||||||
|
player_mid (int): Id of the player message.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
discord.Message | None: Player message or None.
|
||||||
|
"""
|
||||||
|
logging.debug(f"[VC] Fetching player message {player_mid}...")
|
||||||
|
|
||||||
|
if not ctx.guild_id:
|
||||||
|
logging.warning("[VC] Guild ID not found in context")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
if isinstance(ctx, Interaction):
|
||||||
|
player = ctx.client.get_message(player_mid)
|
||||||
|
elif isinstance(ctx, RawReactionActionEvent):
|
||||||
|
if not self.bot:
|
||||||
|
raise ValueError("Bot instance is not set.")
|
||||||
|
player = self.bot.get_message(player_mid)
|
||||||
|
elif isinstance(ctx, ApplicationContext):
|
||||||
|
player = await ctx.fetch_message(player_mid)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid context type: '{type(ctx).__name__}'.")
|
||||||
|
except discord.DiscordException as e:
|
||||||
|
logging.debug(f"[VC] Failed to get player message: {e}")
|
||||||
|
self.db.update(ctx.guild_id, {'current_menu': None})
|
||||||
|
return None
|
||||||
|
|
||||||
|
if player:
|
||||||
|
logging.debug("[VC] Player message found")
|
||||||
|
else:
|
||||||
|
logging.debug("[VC] Player message not found. Resetting current_menu field.")
|
||||||
|
self.db.update(ctx.guild_id, {'current_menu': None})
|
||||||
|
|
||||||
|
return player
|
||||||
|
|
||||||
async def update_menu_embed(
|
async def update_menu_embed(
|
||||||
self,
|
self,
|
||||||
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
|
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
|
||||||
player_mid: int,
|
menu_mid: int,
|
||||||
button_callback: bool = False
|
button_callback: bool = False
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Update current player message by its id. Return True if updated, False if not.
|
"""Update current player message by its id. Return True if updated, False if not.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ctx (ApplicationContext | Interaction): Context.
|
ctx (ApplicationContext | Interaction): Context.
|
||||||
player_mid (int): Id of the player message. There can only be only one player in the guild.
|
menu_mid (int): Id of the player message. There can only be only one player in the guild.
|
||||||
button_callback (bool, optional): If True, the interaction is a button interaction. Defaults to False.
|
button_callback (bool, optional): If True, the interaction is a button interaction. Defaults to False.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -74,32 +115,33 @@ class VoiceExtension:
|
|||||||
"""
|
"""
|
||||||
from MusicBot.ui import MenuView
|
from MusicBot.ui import MenuView
|
||||||
logging.debug(
|
logging.debug(
|
||||||
f"Updating player embed using " + (
|
f"[VC] Updating player embed using " + (
|
||||||
"interaction context" if isinstance(ctx, Interaction) else
|
"interaction context" if isinstance(ctx, Interaction) else
|
||||||
"application context" if isinstance(ctx, ApplicationContext) else
|
"application context" if isinstance(ctx, ApplicationContext) else
|
||||||
"raw reaction context")
|
"raw reaction context"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None
|
gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None
|
||||||
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
|
||||||
|
|
||||||
if not gid or not uid:
|
if not gid or not uid:
|
||||||
logging.warning("Guild ID or User ID not found in context inside 'update_player_embed'")
|
logging.warning("[VC] Guild ID or User ID not found in context inside 'update_player_embed'")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
player = await self.get_menu_message(ctx, player_mid)
|
player = await self.get_menu_message(ctx, menu_mid)
|
||||||
if not player:
|
if not player:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
token = self.users_db.get_ym_token(uid)
|
token = self.users_db.get_ym_token(uid)
|
||||||
if not token:
|
if not token:
|
||||||
logging.debug(f"No token found for user {uid}")
|
logging.debug(f"[VC] No token found for user {uid}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
guild = self.db.get_guild(gid)
|
guild = self.db.get_guild(gid)
|
||||||
current_track = guild['current_track']
|
current_track = guild['current_track']
|
||||||
if not current_track:
|
if not current_track:
|
||||||
logging.debug("No current track found")
|
logging.debug("[VC] No current track found")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
track = cast(Track, Track.de_json(
|
track = cast(Track, Track.de_json(
|
||||||
@@ -117,56 +159,54 @@ class VoiceExtension:
|
|||||||
# 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 player.edit(embed=embed, view=await MenuView(ctx).init())
|
await player.edit(embed=embed, view=await MenuView(ctx).init())
|
||||||
except discord.NotFound:
|
except discord.NotFound:
|
||||||
|
logging.warning("[VC] Player message not found")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def update_vibe(
|
async def update_vibe(
|
||||||
self, ctx: ApplicationContext | Interaction,
|
self,
|
||||||
type: Literal['track', 'album', 'artist', 'playlist', 'user'] | None = None,
|
ctx: ApplicationContext | Interaction,
|
||||||
id: str | int | None = None,
|
type: Literal['track', 'album', 'artist', 'playlist', 'user'],
|
||||||
|
id: str | int,
|
||||||
|
*,
|
||||||
button_callback: bool = False
|
button_callback: bool = False
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""Get next vibe track. Return track title on success. If type or id is None, user's vibe will be used.
|
"""Update vibe state. Return track title on success.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ctx (ApplicationContext | Interaction): Context.
|
ctx (ApplicationContext | Interaction): Context.
|
||||||
type (Literal['track', 'album', 'artist', 'playlist', 'user'] | None, optional): Type of the item. Defaults to None.
|
type (Literal['track', 'album', 'artist', 'playlist', 'user']): Type of the item.
|
||||||
id (str | int | Literal['onyourwave'] | None, optional): ID of the item. Defaults to None.
|
id (str | int): ID of the item.
|
||||||
button_callback (bool, optional): If the function is called from button callback. Defaults to False.
|
button_callback (bool, optional): If the function is called from button callback. Defaults to False.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str | None: Track title or None.
|
str | None: Track title or None.
|
||||||
"""
|
"""
|
||||||
logging.info(f"Updating vibe for guild {ctx.guild_id} with type '{type}' and id '{id}'")
|
logging.info(f"[VC] Updating vibe for guild {ctx.guild_id} with type '{type}' and id '{id}'")
|
||||||
|
|
||||||
gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None
|
gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None
|
||||||
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
|
||||||
if not uid or not gid:
|
if not uid or not gid:
|
||||||
logging.warning("Guild ID or User ID not found in context inside 'vibe_update'")
|
logging.warning("[VC] Guild ID or User ID not found in context inside 'vibe_update'")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
token = self.users_db.get_ym_token(uid)
|
token = self.users_db.get_ym_token(uid)
|
||||||
if not token:
|
if not token:
|
||||||
logging.info(f"User {uid} has no YM token")
|
logging.info(f"[VC] User {uid} has no YM token")
|
||||||
await ctx.respond("❌ Укажите токен через /account login.", ephemeral=True)
|
await ctx.respond("❌ Укажите токен через /account login.", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
client = await YMClient(token).init()
|
client = await YMClient(token).init()
|
||||||
except yandex_music.exceptions.UnauthorizedError:
|
except yandex_music.exceptions.UnauthorizedError:
|
||||||
logging.info(f"User {uid} provided invalid token")
|
logging.info(f"[VC] User {uid} provided invalid token")
|
||||||
await ctx.respond('❌ Недействительный токен.')
|
await ctx.respond('❌ Недействительный токен.')
|
||||||
return
|
return
|
||||||
|
|
||||||
if type and id:
|
self.users_db.update(uid, {'vibe_type': type, 'vibe_id': id})
|
||||||
self.users_db.update(uid, {'vibe_type': type, 'vibe_id': id})
|
|
||||||
else:
|
|
||||||
logging.info(f"[VIBE] Using user's vibe for guild {gid}")
|
|
||||||
type = 'user'
|
|
||||||
id = 'onyourwave'
|
|
||||||
|
|
||||||
guild = self.db.get_guild(gid)
|
guild = self.db.get_guild(gid)
|
||||||
|
|
||||||
if not guild['vibing']:
|
if not guild['vibing']:
|
||||||
feedback = await client.rotor_station_feedback_radio_started(
|
feedback = await client.rotor_station_feedback_radio_started(
|
||||||
f"{type}:{id}",
|
f"{type}:{id}",
|
||||||
@@ -175,9 +215,7 @@ class VoiceExtension:
|
|||||||
)
|
)
|
||||||
logging.debug(f"[VIBE] Radio started feedback: {feedback}")
|
logging.debug(f"[VIBE] Radio started feedback: {feedback}")
|
||||||
|
|
||||||
tracks = await client.rotor_station_tracks(
|
tracks = await client.rotor_station_tracks(f"{type}:{id}")
|
||||||
f"{type}:{id}"
|
|
||||||
)
|
|
||||||
self.db.update(gid, {'vibing': True})
|
self.db.update(gid, {'vibing': True})
|
||||||
elif guild['current_track']:
|
elif guild['current_track']:
|
||||||
tracks = await client.rotor_station_tracks(
|
tracks = await client.rotor_station_tracks(
|
||||||
@@ -197,50 +235,12 @@ class VoiceExtension:
|
|||||||
|
|
||||||
next_tracks = [cast(Track, track.track) for track in tracks.sequence]
|
next_tracks = [cast(Track, track.track) for track in tracks.sequence]
|
||||||
|
|
||||||
self.db.update(gid, {'next_tracks': [track.to_dict() for track in next_tracks[1:]]})
|
self.db.update(gid, {
|
||||||
|
'next_tracks': [track.to_dict() for track in next_tracks[1:]],
|
||||||
|
'current_viber_id': uid
|
||||||
|
})
|
||||||
await self.stop_playing(ctx)
|
await self.stop_playing(ctx)
|
||||||
return await self.play_track(ctx, next_tracks[0], button_callback=button_callback)
|
return await self.play_track(ctx, next_tracks[0], button_callback=button_callback)
|
||||||
|
|
||||||
async def get_menu_message(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, player_mid: int) -> discord.Message | None:
|
|
||||||
"""Fetch the player message by its id. Return the message if found, None if not.
|
|
||||||
Reset `current_menu` field in the database if not found.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
ctx (ApplicationContext | Interaction): Context.
|
|
||||||
player_mid (int): Id of the player message.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
discord.Message | None: Player message or None.
|
|
||||||
"""
|
|
||||||
logging.debug(f"Fetching player message {player_mid}...")
|
|
||||||
|
|
||||||
if not ctx.guild_id:
|
|
||||||
logging.warning("Guild ID not found in context")
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
if isinstance(ctx, Interaction):
|
|
||||||
player = ctx.client.get_message(player_mid)
|
|
||||||
elif isinstance(ctx, RawReactionActionEvent):
|
|
||||||
if not self.bot:
|
|
||||||
raise ValueError("Bot instance is not set.")
|
|
||||||
player = self.bot.get_message(player_mid)
|
|
||||||
elif isinstance(ctx, ApplicationContext):
|
|
||||||
player = await ctx.fetch_message(player_mid)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Invalid context type: '{type(ctx).__name__}'.")
|
|
||||||
except discord.DiscordException as e:
|
|
||||||
logging.debug(f"Failed to get player message: {e}")
|
|
||||||
self.db.update(ctx.guild_id, {'current_menu': None})
|
|
||||||
return None
|
|
||||||
|
|
||||||
if player:
|
|
||||||
logging.debug(f"Player message found")
|
|
||||||
else:
|
|
||||||
logging.debug("Player message not found. Resetting current_menu field.")
|
|
||||||
self.db.update(ctx.guild_id, {'current_menu': None})
|
|
||||||
|
|
||||||
return player
|
|
||||||
|
|
||||||
async def voice_check(self, ctx: ApplicationContext | Interaction) -> bool:
|
async def voice_check(self, ctx: ApplicationContext | Interaction) -> bool:
|
||||||
"""Check if bot can perform voice tasks and respond if failed.
|
"""Check if bot can perform voice tasks and respond if failed.
|
||||||
@@ -251,33 +251,36 @@ class VoiceExtension:
|
|||||||
Returns:
|
Returns:
|
||||||
bool: Check result.
|
bool: Check result.
|
||||||
"""
|
"""
|
||||||
if not ctx.user:
|
if not ctx.user or not ctx.guild:
|
||||||
logging.warning("User not found in context inside 'voice_check'")
|
logging.warning("[VC] User or guild not found in context inside 'voice_check'")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
token = self.users_db.get_ym_token(ctx.user.id)
|
token = self.users_db.get_ym_token(ctx.user.id)
|
||||||
if not token:
|
if not token:
|
||||||
logging.debug(f"No token found for user {ctx.user.id}")
|
logging.debug(f"[VC] No token found for user {ctx.user.id}")
|
||||||
await ctx.respond("❌ Необходимо указать свой токен доступа с помощью команды /login.", delete_after=15, ephemeral=True)
|
await ctx.respond("❌ Необходимо указать свой токен доступа с помощью команды /login.", delete_after=15, ephemeral=True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
channel = ctx.channel
|
if not isinstance(ctx.channel, discord.VoiceChannel):
|
||||||
if not isinstance(channel, discord.VoiceChannel):
|
logging.debug("[VC] User is not in a voice channel")
|
||||||
logging.debug("User is not in a voice channel")
|
|
||||||
await ctx.respond("❌ Вы должны отправить команду в голосовом канале.", delete_after=15, ephemeral=True)
|
await ctx.respond("❌ Вы должны отправить команду в голосовом канале.", delete_after=15, ephemeral=True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if isinstance(ctx, Interaction):
|
voice_clients = ctx.client.voice_clients if isinstance(ctx, Interaction) else ctx.bot.voice_clients
|
||||||
channels = ctx.client.voice_clients
|
voice_chat = discord.utils.get(voice_clients, guild=ctx.guild)
|
||||||
else:
|
|
||||||
channels = ctx.bot.voice_clients
|
|
||||||
voice_chat = discord.utils.get(channels, guild=ctx.guild)
|
|
||||||
if not voice_chat:
|
if not voice_chat:
|
||||||
logging.debug("Voice client not found")
|
logging.debug("[VC] Voice client not found")
|
||||||
await ctx.respond("❌ Добавьте бота в голосовой канал при помощи команды /voice join.", delete_after=15, ephemeral=True)
|
await ctx.respond("❌ Добавьте бота в голосовой канал при помощи команды /voice join.", delete_after=15, ephemeral=True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
logging.debug("Voice requirements met")
|
guild = self.db.get_guild(ctx.guild.id)
|
||||||
|
member = cast(discord.Member, ctx.user)
|
||||||
|
if guild['vibing'] and ctx.user.id != guild['current_viber_id'] and not member.guild_permissions.manage_channels:
|
||||||
|
logging.debug("[VIBE] Context user is not the current viber")
|
||||||
|
await ctx.respond("❌ Вы не можете взаимодействовать с чужой волной!", delete_after=15, ephemeral=True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
logging.debug("[VC] Voice requirements met")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def get_voice_client(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent) -> discord.VoiceClient | None:
|
async def get_voice_client(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent) -> discord.VoiceClient | None:
|
||||||
@@ -290,25 +293,30 @@ class VoiceExtension:
|
|||||||
discord.VoiceClient | None: Voice client or None.
|
discord.VoiceClient | None: Voice client or None.
|
||||||
"""
|
"""
|
||||||
if isinstance(ctx, Interaction):
|
if isinstance(ctx, Interaction):
|
||||||
voice_chat = discord.utils.get(ctx.client.voice_clients, guild=ctx.guild)
|
voice_clients = ctx.client.voice_clients
|
||||||
|
guild = ctx.guild
|
||||||
elif isinstance(ctx, RawReactionActionEvent):
|
elif isinstance(ctx, RawReactionActionEvent):
|
||||||
if not self.bot:
|
if not self.bot:
|
||||||
raise ValueError("Bot instance is not set.")
|
raise ValueError("Bot instance is not set.")
|
||||||
if not ctx.guild_id:
|
if not ctx.guild_id:
|
||||||
logging.warning("Guild ID not found in context inside get_voice_client")
|
logging.warning("[VC] Guild ID not found in context inside get_voice_client")
|
||||||
return None
|
return None
|
||||||
voice_chat = discord.utils.get(self.bot.voice_clients, guild=await self.bot.fetch_guild(ctx.guild_id))
|
voice_clients = self.bot.voice_clients
|
||||||
|
guild = await self.bot.fetch_guild(ctx.guild_id)
|
||||||
elif isinstance(ctx, ApplicationContext):
|
elif isinstance(ctx, ApplicationContext):
|
||||||
voice_chat = discord.utils.get(ctx.bot.voice_clients, guild=ctx.guild)
|
voice_clients = ctx.bot.voice_clients
|
||||||
|
guild = ctx.guild
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Invalid context type: '{type(ctx).__name__}'.")
|
raise ValueError(f"Invalid context type: '{type(ctx).__name__}'.")
|
||||||
|
|
||||||
if voice_chat:
|
voice_chat = discord.utils.get(voice_clients, guild=guild)
|
||||||
logging.debug("Voice client found")
|
|
||||||
else:
|
|
||||||
logging.debug("Voice client not found")
|
|
||||||
|
|
||||||
return cast((discord.VoiceClient | None), voice_chat)
|
if voice_chat:
|
||||||
|
logging.debug("[VC] Voice client found")
|
||||||
|
else:
|
||||||
|
logging.debug("[VC] Voice client not found")
|
||||||
|
|
||||||
|
return cast(discord.VoiceClient | None, voice_chat)
|
||||||
|
|
||||||
async def play_track(
|
async def play_track(
|
||||||
self,
|
self,
|
||||||
@@ -316,7 +324,9 @@ class VoiceExtension:
|
|||||||
track: Track,
|
track: Track,
|
||||||
*,
|
*,
|
||||||
vc: discord.VoiceClient | None = None,
|
vc: discord.VoiceClient | None = None,
|
||||||
button_callback: bool = False
|
menu_message: discord.Message | None = None,
|
||||||
|
button_callback: bool = False,
|
||||||
|
retry: bool = False
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""Download ``track`` by its id and play it in the voice channel. Return track title on success.
|
"""Download ``track`` by its id and play it in the voice channel. Return track title on success.
|
||||||
If sound is already playing, add track id to the queue. There's no response to the context.
|
If sound is already playing, add track id to the queue. There's no response to the context.
|
||||||
@@ -325,15 +335,19 @@ class VoiceExtension:
|
|||||||
ctx (ApplicationContext | Interaction): Context
|
ctx (ApplicationContext | Interaction): Context
|
||||||
track (Track): Track to play.
|
track (Track): Track to play.
|
||||||
vc (discord.VoiceClient | None): Voice client.
|
vc (discord.VoiceClient | None): Voice client.
|
||||||
|
menu_message (discord.Message | None): Menu message.
|
||||||
button_callback (bool): Whether the interaction is a button callback.
|
button_callback (bool): Whether the interaction is a button callback.
|
||||||
|
retry (bool): Whether the function is called again.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str | None: Song title or None.
|
str | None: Song title or None.
|
||||||
"""
|
"""
|
||||||
|
from MusicBot.ui import MenuView
|
||||||
|
|
||||||
gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None
|
gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None
|
||||||
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
|
||||||
if not gid or not uid:
|
if not gid or not uid:
|
||||||
logging.warning("Guild ID or User ID not found in context inside 'play_track'")
|
logging.warning("[VC] Guild ID or User ID not found in context inside 'play_track'")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not vc:
|
if not vc:
|
||||||
@@ -352,25 +366,36 @@ class VoiceExtension:
|
|||||||
else:
|
else:
|
||||||
raise ValueError(f"Invalid context type: '{type(ctx).__name__}'.")
|
raise ValueError(f"Invalid context type: '{type(ctx).__name__}'.")
|
||||||
|
|
||||||
|
self.db.set_current_track(gid, track)
|
||||||
guild = self.db.get_guild(gid)
|
guild = self.db.get_guild(gid)
|
||||||
|
if guild['current_menu'] and not isinstance(ctx, RawReactionActionEvent):
|
||||||
|
if menu_message:
|
||||||
|
try:
|
||||||
|
await menu_message.edit(embed=await generate_item_embed(track, guild['vibing']), view=await MenuView(ctx).init())
|
||||||
|
except discord.errors.NotFound:
|
||||||
|
logging.warning("[VC] Menu message not found. Using 'update_menu_embed' instead.")
|
||||||
|
await self._retry_update_menu_embed(ctx, guild['current_menu'], button_callback)
|
||||||
|
else:
|
||||||
|
await self._retry_update_menu_embed(ctx, guild['current_menu'], button_callback)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await track.download_async(f'music/{gid}.mp3')
|
await track.download_async(f'music/{gid}.mp3')
|
||||||
song = discord.FFmpegPCMAudio(f'music/{gid}.mp3', options='-vn -filter:a "volume=0.15"')
|
song = discord.FFmpegPCMAudio(f'music/{gid}.mp3', options='-vn -filter:a "volume=0.15"')
|
||||||
except yandex_music.exceptions.TimedOutError: # Not sure why that happens. Probably should add timeout for buttons.
|
except yandex_music.exceptions.TimedOutError: # sometimes track takes too long to download.
|
||||||
|
logging.warning(f"[VC] Timed out while downloading track '{track.title}'")
|
||||||
if not isinstance(ctx, RawReactionActionEvent) and ctx.user and ctx.channel:
|
if not isinstance(ctx, RawReactionActionEvent) and ctx.user and ctx.channel:
|
||||||
channel = cast(discord.VoiceChannel, ctx.channel)
|
channel = cast(discord.VoiceChannel, ctx.channel)
|
||||||
await channel.send(f"😔 {ctx.user.mention}, не удалось загрузить трек. Яндекс Музыка не отвечает или блокирует запросы.")
|
if not retry:
|
||||||
|
channel = cast(discord.VoiceChannel, ctx.channel)
|
||||||
|
await channel.send(f"Не удалось загрузить трек. Пробуем заного...", delete_after=5)
|
||||||
|
return await self.play_track(ctx, track, vc=vc, button_callback=button_callback, retry=True)
|
||||||
|
await channel.send(f"😔 Снова не удалось загрузить трек. Попробуйте сбросить меню.", delete_after=15)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
vc.play(song, after=lambda exc: asyncio.run_coroutine_threadsafe(self.next_track(ctx, after=True), loop))
|
vc.play(song, after=lambda exc: asyncio.run_coroutine_threadsafe(self.next_track(ctx, after=True), loop))
|
||||||
logging.info(f"Playing track '{track.title}'")
|
logging.info(f"[VC] Playing track '{track.title}'")
|
||||||
|
|
||||||
self.db.set_current_track(gid, track)
|
|
||||||
self.db.update(gid, {'is_stopped': False})
|
self.db.update(gid, {'is_stopped': False})
|
||||||
|
|
||||||
player = guild['current_menu']
|
|
||||||
if player is not None:
|
|
||||||
await self.update_menu_embed(ctx, player, button_callback)
|
|
||||||
|
|
||||||
if guild['vibing']:
|
if guild['vibing']:
|
||||||
user = self.users_db.get_user(uid)
|
user = self.users_db.get_user(uid)
|
||||||
@@ -388,13 +413,13 @@ class VoiceExtension:
|
|||||||
|
|
||||||
gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None
|
gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None
|
||||||
if not gid:
|
if not gid:
|
||||||
logging.warning("Guild ID not found in context")
|
logging.warning("[VC] Guild ID not found in context")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not vc:
|
if not vc:
|
||||||
vc = await self.get_voice_client(ctx)
|
vc = await self.get_voice_client(ctx)
|
||||||
if vc:
|
if vc:
|
||||||
logging.debug("Stopping playback")
|
logging.debug("[VC] Stopping playback")
|
||||||
self.db.update(gid, {'current_track': None, 'is_stopped': True})
|
self.db.update(gid, {'current_track': None, 'is_stopped': True})
|
||||||
vc.stop()
|
vc.stop()
|
||||||
|
|
||||||
@@ -418,8 +443,12 @@ class VoiceExtension:
|
|||||||
Returns:
|
Returns:
|
||||||
str | None: Track title or None.
|
str | None: Track title or None.
|
||||||
"""
|
"""
|
||||||
|
from MusicBot.ui import MenuView
|
||||||
|
|
||||||
gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None
|
gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None
|
||||||
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
|
||||||
|
menu_message = None
|
||||||
|
|
||||||
if not gid or not uid:
|
if not gid or not uid:
|
||||||
logging.warning("Guild ID or User ID not found in context inside 'next_track'")
|
logging.warning("Guild ID or User ID not found in context inside 'next_track'")
|
||||||
return None
|
return None
|
||||||
@@ -430,7 +459,7 @@ class VoiceExtension:
|
|||||||
if not token:
|
if not token:
|
||||||
logging.debug(f"No token found for user {uid}")
|
logging.debug(f"No token found for user {uid}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
client = await YMClient(token).init()
|
client = await YMClient(token).init()
|
||||||
|
|
||||||
if guild['is_stopped'] and after:
|
if guild['is_stopped'] and after:
|
||||||
@@ -442,7 +471,16 @@ class VoiceExtension:
|
|||||||
if not vc: # Silently return if bot got kicked
|
if not vc: # Silently return if bot got kicked
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if after and guild['current_menu']:
|
||||||
|
menu_message = await self.get_menu_message(ctx, guild['current_menu'])
|
||||||
|
if menu_message:
|
||||||
|
await menu_message.edit(view=await MenuView(ctx).init(disable=True))
|
||||||
|
|
||||||
if guild['vibing'] and not isinstance(ctx, RawReactionActionEvent):
|
if guild['vibing'] and not isinstance(ctx, RawReactionActionEvent):
|
||||||
|
if not user['vibe_type'] or not user['vibe_id']:
|
||||||
|
logging.warning("[VIBE] No vibe type or id found")
|
||||||
|
return None
|
||||||
|
|
||||||
if guild['current_track']:
|
if guild['current_track']:
|
||||||
if after:
|
if after:
|
||||||
res = await client.rotor_station_feedback_track_finished(
|
res = await client.rotor_station_feedback_track_finished(
|
||||||
@@ -462,8 +500,13 @@ class VoiceExtension:
|
|||||||
time()
|
time()
|
||||||
)
|
)
|
||||||
logging.debug(f"[VIBE] Skipped track: {res}")
|
logging.debug(f"[VIBE] Skipped track: {res}")
|
||||||
return await self.update_vibe(ctx, user['vibe_type'], user['vibe_id'], button_callback)
|
return await self.update_vibe(
|
||||||
|
ctx,
|
||||||
|
user['vibe_type'],
|
||||||
|
user['vibe_id'],
|
||||||
|
button_callback=button_callback
|
||||||
|
)
|
||||||
|
|
||||||
if guild['repeat'] and after:
|
if guild['repeat'] and after:
|
||||||
logging.debug("Repeating current track")
|
logging.debug("Repeating current track")
|
||||||
next_track = guild['current_track']
|
next_track = guild['current_track']
|
||||||
@@ -488,6 +531,7 @@ class VoiceExtension:
|
|||||||
ctx,
|
ctx,
|
||||||
ym_track, # type: ignore # de_json should always work here.
|
ym_track, # type: ignore # de_json should always work here.
|
||||||
vc=vc,
|
vc=vc,
|
||||||
|
menu_message=menu_message,
|
||||||
button_callback=button_callback
|
button_callback=button_callback
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -495,9 +539,19 @@ class VoiceExtension:
|
|||||||
await ctx.respond(f"Сейчас играет: **{title}**!", delete_after=15)
|
await ctx.respond(f"Сейчас играет: **{title}**!", delete_after=15)
|
||||||
|
|
||||||
return title
|
return title
|
||||||
|
|
||||||
elif guild['vibing'] and not isinstance(ctx, RawReactionActionEvent):
|
elif guild['vibing'] and not isinstance(ctx, RawReactionActionEvent):
|
||||||
logging.debug("[VIBE] No next track found, updating vibe")
|
logging.debug("[VIBE] No next track found, updating vibe")
|
||||||
return await self.update_vibe(ctx, user['vibe_type'], user['vibe_id'], button_callback)
|
if not user['vibe_type'] or not user['vibe_id']:
|
||||||
|
logging.warning("[VIBE] No vibe type or id found")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return await self.update_vibe(
|
||||||
|
ctx,
|
||||||
|
user['vibe_type'],
|
||||||
|
user['vibe_id'],
|
||||||
|
button_callback=button_callback
|
||||||
|
)
|
||||||
|
|
||||||
logging.info("No next track found")
|
logging.info("No next track found")
|
||||||
self.db.update(gid, {'is_stopped': True, 'current_track': None})
|
self.db.update(gid, {'is_stopped': True, 'current_track': None})
|
||||||
@@ -553,14 +607,14 @@ class VoiceExtension:
|
|||||||
|
|
||||||
async def get_likes(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent) -> list[TrackShort] | None:
|
async def get_likes(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent) -> list[TrackShort] | None:
|
||||||
"""Get liked tracks. Return list of tracks on success. Return None if no token found.
|
"""Get liked tracks. Return list of tracks on success. Return None if no token found.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ctx (ApplicationContext | Interaction): Context.
|
ctx (ApplicationContext | Interaction): Context.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list[Track] | None: List of tracks or None.
|
list[Track] | None: List of tracks or None.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None
|
gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None
|
||||||
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
|
||||||
if not gid or not uid:
|
if not gid or not uid:
|
||||||
@@ -583,7 +637,7 @@ class VoiceExtension:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
return likes.tracks
|
return likes.tracks
|
||||||
|
|
||||||
async def like_track(self, ctx: ApplicationContext | Interaction) -> str | Literal['TRACK REMOVED'] | None:
|
async def like_track(self, ctx: ApplicationContext | Interaction) -> str | Literal['TRACK REMOVED'] | None:
|
||||||
"""Like current track. Return track title on success.
|
"""Like current track. Return track title on success.
|
||||||
|
|
||||||
@@ -623,4 +677,16 @@ class VoiceExtension:
|
|||||||
logging.debug("Client account not found")
|
logging.debug("Client account not found")
|
||||||
return None
|
return None
|
||||||
await client.users_likes_tracks_remove(ym_track.id, client.me.account.uid)
|
await client.users_likes_tracks_remove(ym_track.id, client.me.account.uid)
|
||||||
return 'TRACK REMOVED'
|
return 'TRACK REMOVED'
|
||||||
|
|
||||||
|
async def _retry_update_menu_embed(
|
||||||
|
self,
|
||||||
|
ctx: ApplicationContext | Interaction,
|
||||||
|
menu_mid: int,
|
||||||
|
button_callback: bool
|
||||||
|
) -> None:
|
||||||
|
update = await self.update_menu_embed(ctx, menu_mid, button_callback)
|
||||||
|
for _ in range(10):
|
||||||
|
if update:
|
||||||
|
break
|
||||||
|
update = await self.update_menu_embed(ctx, menu_mid, button_callback)
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ class Voice(Cog, VoiceExtension):
|
|||||||
await message.clear_reactions()
|
await message.clear_reactions()
|
||||||
await message.edit(content='Запрос был отклонён.', delete_after=15)
|
await message.edit(content='Запрос был отклонён.', delete_after=15)
|
||||||
del votes[str(payload.message_id)]
|
del votes[str(payload.message_id)]
|
||||||
|
|
||||||
self.db.update(guild_id, {'votes': votes})
|
self.db.update(guild_id, {'votes': votes})
|
||||||
|
|
||||||
@Cog.listener()
|
@Cog.listener()
|
||||||
@@ -165,7 +165,7 @@ class Voice(Cog, VoiceExtension):
|
|||||||
logging.info(f"Reaction removed by user {payload.user_id} in channel {payload.channel_id}")
|
logging.info(f"Reaction removed by user {payload.user_id} in channel {payload.channel_id}")
|
||||||
if not self.typed_bot.user:
|
if not self.typed_bot.user:
|
||||||
return
|
return
|
||||||
|
|
||||||
guild_id = payload.guild_id
|
guild_id = payload.guild_id
|
||||||
if not guild_id:
|
if not guild_id:
|
||||||
return
|
return
|
||||||
@@ -187,7 +187,7 @@ class Voice(Cog, VoiceExtension):
|
|||||||
elif payload.emoji.name == '❌':
|
elif payload.emoji.name == '❌':
|
||||||
logging.info(f"User {payload.user_id} removed negative vote for message {payload.message_id}")
|
logging.info(f"User {payload.user_id} removed negative vote for message {payload.message_id}")
|
||||||
del vote_data['negative_votes'][payload.user_id]
|
del vote_data['negative_votes'][payload.user_id]
|
||||||
|
|
||||||
self.db.update(guild_id, {'votes': votes})
|
self.db.update(guild_id, {'votes': votes})
|
||||||
|
|
||||||
@voice.command(name="menu", description="Создать меню проигрывателя. Доступно только если вы единственный в голосовом канале.")
|
@voice.command(name="menu", description="Создать меню проигрывателя. Доступно только если вы единственный в голосовом канале.")
|
||||||
@@ -231,7 +231,7 @@ class Voice(Cog, VoiceExtension):
|
|||||||
logging.info(f"User {ctx.author.id} does not have permissions to execute leave command in guild {ctx.guild.id}")
|
logging.info(f"User {ctx.author.id} does not have permissions to execute leave command in guild {ctx.guild.id}")
|
||||||
await ctx.respond("❌ У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True)
|
await ctx.respond("❌ У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
vc = await self.get_voice_client(ctx)
|
vc = await self.get_voice_client(ctx)
|
||||||
if await self.voice_check(ctx) and vc:
|
if await self.voice_check(ctx) and vc:
|
||||||
self.db.update(ctx.guild.id, {'previous_tracks': [], 'next_tracks': [], 'current_track': None, 'is_stopped': True})
|
self.db.update(ctx.guild.id, {'previous_tracks': [], 'next_tracks': [], 'current_track': None, 'is_stopped': True})
|
||||||
@@ -440,9 +440,9 @@ class Voice(Cog, VoiceExtension):
|
|||||||
logging.info(f"Track added to favorites for user {ctx.author.id} in guild {ctx.guild.id}")
|
logging.info(f"Track added to favorites for user {ctx.author.id} in guild {ctx.guild.id}")
|
||||||
await ctx.respond(f"Трек **{result}** был добавлен в избранное.", delete_after=15, ephemeral=True)
|
await ctx.respond(f"Трек **{result}** был добавлен в избранное.", delete_after=15, ephemeral=True)
|
||||||
|
|
||||||
@track.command(description="Запустить мою волну по текущему треку.")
|
@track.command(name='vibe', description="Запустить мою волну по текущему треку.")
|
||||||
async def vibe(self, ctx: discord.ApplicationContext) -> None:
|
async def track_vibe(self, ctx: discord.ApplicationContext) -> None:
|
||||||
logging.info(f"Vibe command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
|
logging.info(f"Vibe (track) command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
|
||||||
if not await self.voice_check(ctx):
|
if not await self.voice_check(ctx):
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -460,3 +460,20 @@ class Voice(Cog, VoiceExtension):
|
|||||||
|
|
||||||
await self.send_menu_message(ctx)
|
await self.send_menu_message(ctx)
|
||||||
await self.update_vibe(ctx, 'track', guild['current_track']['id'])
|
await self.update_vibe(ctx, 'track', guild['current_track']['id'])
|
||||||
|
|
||||||
|
@discord.slash_command(name='vibe', description="Запустить Мою Волну.")
|
||||||
|
async def user_vibe(self, ctx: discord.ApplicationContext) -> None:
|
||||||
|
logging.info(f"Vibe (user) command invoked by user {ctx.user.id} in guild {ctx.guild_id}")
|
||||||
|
if not await self.voice_check(ctx):
|
||||||
|
return
|
||||||
|
|
||||||
|
guild = self.db.get_guild(ctx.guild.id)
|
||||||
|
channel = cast(discord.VoiceChannel, ctx.channel)
|
||||||
|
|
||||||
|
if len(channel.members) > 2 and not guild['always_allow_menu']:
|
||||||
|
logging.info(f"Action declined: other members are present in the voice channel")
|
||||||
|
await ctx.respond("❌ Вы не единственный в голосовом канале.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.send_menu_message(ctx)
|
||||||
|
await self.update_vibe(ctx, 'user', 'onyourwave')
|
||||||
|
|||||||
@@ -106,7 +106,8 @@ class BaseGuildsDatabase:
|
|||||||
shuffle=False,
|
shuffle=False,
|
||||||
repeat=False,
|
repeat=False,
|
||||||
votes={},
|
votes={},
|
||||||
vibing=False
|
vibing=False,
|
||||||
|
current_viber_id=None
|
||||||
))
|
))
|
||||||
|
|
||||||
def update(self, gid: int, data: Guild) -> None:
|
def update(self, gid: int, data: Guild) -> None:
|
||||||
@@ -152,7 +153,8 @@ class BaseGuildsDatabase:
|
|||||||
shuffle=False,
|
shuffle=False,
|
||||||
repeat=False,
|
repeat=False,
|
||||||
votes={},
|
votes={},
|
||||||
vibing=False
|
vibing=False,
|
||||||
|
current_viber_id=None
|
||||||
)
|
)
|
||||||
for field, default_value in fields.items():
|
for field, default_value in fields.items():
|
||||||
if field not in existing_fields:
|
if field not in existing_fields:
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class Guild(TypedDict, total=False):
|
|||||||
repeat: bool
|
repeat: bool
|
||||||
votes: dict[str, MessageVotes]
|
votes: dict[str, MessageVotes]
|
||||||
vibing: bool
|
vibing: bool
|
||||||
|
current_viber_id: int | None
|
||||||
|
|
||||||
class ExplicitGuild(TypedDict):
|
class ExplicitGuild(TypedDict):
|
||||||
_id: int
|
_id: int
|
||||||
@@ -42,4 +43,5 @@ class ExplicitGuild(TypedDict):
|
|||||||
shuffle: bool
|
shuffle: bool
|
||||||
repeat: bool
|
repeat: bool
|
||||||
votes: dict[str, MessageVotes]
|
votes: dict[str, MessageVotes]
|
||||||
vibing: bool
|
vibing: bool
|
||||||
|
current_viber_id: int | None
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Literal, cast
|
from typing import Any, Literal, cast
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from yandex_music import Track, Album, Artist, Playlist
|
from yandex_music import Track, Album, Artist, Playlist
|
||||||
@@ -19,11 +19,11 @@ class PlayButton(Button, VoiceExtension):
|
|||||||
logging.debug(f"Callback triggered for type: '{type(self.item).__name__}'")
|
logging.debug(f"Callback triggered for type: '{type(self.item).__name__}'")
|
||||||
|
|
||||||
if not interaction.guild:
|
if not interaction.guild:
|
||||||
logging.warning("No guild found in context.")
|
logging.warning("No guild found in PlayButton callback")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not await self.voice_check(interaction):
|
if not await self.voice_check(interaction):
|
||||||
logging.debug("Voice check failed")
|
logging.debug("Voice check failed in PlayButton callback")
|
||||||
return
|
return
|
||||||
|
|
||||||
gid = interaction.guild.id
|
gid = interaction.guild.id
|
||||||
@@ -41,7 +41,7 @@ class PlayButton(Button, VoiceExtension):
|
|||||||
elif isinstance(self.item, Album):
|
elif isinstance(self.item, Album):
|
||||||
album = await self.item.with_tracks_async()
|
album = await self.item.with_tracks_async()
|
||||||
if not album or not album.volumes:
|
if not album or not album.volumes:
|
||||||
logging.debug("Failed to fetch album tracks")
|
logging.debug("Failed to fetch album tracks in PlayButton callback")
|
||||||
await interaction.respond("Не удалось получить треки альбома.", ephemeral=True)
|
await interaction.respond("Не удалось получить треки альбома.", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ class PlayButton(Button, VoiceExtension):
|
|||||||
elif isinstance(self.item, Artist):
|
elif isinstance(self.item, Artist):
|
||||||
artist_tracks = await self.item.get_tracks_async()
|
artist_tracks = await self.item.get_tracks_async()
|
||||||
if not artist_tracks:
|
if not artist_tracks:
|
||||||
logging.debug("Failed to fetch artist tracks")
|
logging.debug("Failed to fetch artist tracks in PlayButton callback")
|
||||||
await interaction.respond("Не удалось получить треки артиста.", ephemeral=True)
|
await interaction.respond("Не удалось получить треки артиста.", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ class PlayButton(Button, VoiceExtension):
|
|||||||
elif isinstance(self.item, Playlist):
|
elif isinstance(self.item, Playlist):
|
||||||
short_tracks = await self.item.fetch_tracks_async()
|
short_tracks = await self.item.fetch_tracks_async()
|
||||||
if not short_tracks:
|
if not short_tracks:
|
||||||
logging.debug("Failed to fetch playlist tracks")
|
logging.debug("Failed to fetch playlist tracks in PlayButton callback")
|
||||||
await interaction.respond("❌ Не удалось получить треки из плейлиста.", delete_after=15)
|
await interaction.respond("❌ Не удалось получить треки из плейлиста.", delete_after=15)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ class PlayButton(Button, VoiceExtension):
|
|||||||
elif isinstance(self.item, list):
|
elif isinstance(self.item, list):
|
||||||
tracks = self.item.copy()
|
tracks = self.item.copy()
|
||||||
if not tracks:
|
if not tracks:
|
||||||
logging.debug("Empty tracks list")
|
logging.debug("Empty tracks list in PlayButton callback")
|
||||||
await interaction.respond("❌ Не удалось получить треки.", delete_after=15)
|
await interaction.respond("❌ Не удалось получить треки.", delete_after=15)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ class PlayButton(Button, VoiceExtension):
|
|||||||
raise ValueError(f"Unknown item type: '{type(self.item).__name__}'")
|
raise ValueError(f"Unknown item type: '{type(self.item).__name__}'")
|
||||||
|
|
||||||
if guild.get(f'vote_{action}') and len(channel.members) > 2 and not member.guild_permissions.manage_channels:
|
if guild.get(f'vote_{action}') and len(channel.members) > 2 and not member.guild_permissions.manage_channels:
|
||||||
logging.debug(f"Starting vote for '{action}'")
|
logging.debug(f"Starting vote for '{action}' (from PlayButton callback)")
|
||||||
|
|
||||||
message = cast(discord.Interaction, await interaction.respond(vote_message, delete_after=30))
|
message = cast(discord.Interaction, await interaction.respond(vote_message, delete_after=30))
|
||||||
response = await message.original_response()
|
response = await message.original_response()
|
||||||
@@ -109,7 +109,7 @@ class PlayButton(Button, VoiceExtension):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logging.debug(f"Skipping vote for '{action}'")
|
logging.debug(f"Skipping vote for '{action}' (from PlayButton callback)")
|
||||||
|
|
||||||
if guild['current_track'] is not None:
|
if guild['current_track'] is not None:
|
||||||
self.db.modify_track(gid, tracks, 'next', 'extend')
|
self.db.modify_track(gid, tracks, 'next', 'extend')
|
||||||
@@ -129,6 +129,41 @@ class PlayButton(Button, VoiceExtension):
|
|||||||
else:
|
else:
|
||||||
await interaction.respond(response_message, delete_after=15)
|
await interaction.respond(response_message, delete_after=15)
|
||||||
|
|
||||||
|
class MyVibeButton(Button, VoiceExtension):
|
||||||
|
def __init__(self, item: Track | Album | Artist | Playlist | list[Track], *args, **kwargs):
|
||||||
|
Button.__init__(self, *args, **kwargs)
|
||||||
|
VoiceExtension.__init__(self, None)
|
||||||
|
self.item = item
|
||||||
|
|
||||||
|
async def callback(self, interaction: discord.Interaction):
|
||||||
|
logging.debug(f"[VIBE] Button callback for '{type(self.item).__name__}'")
|
||||||
|
if not await self.voice_check(interaction):
|
||||||
|
return
|
||||||
|
|
||||||
|
gid = interaction.guild_id
|
||||||
|
if not gid:
|
||||||
|
logging.warning(f"[VIBE] Guild ID is None in button callback")
|
||||||
|
return
|
||||||
|
|
||||||
|
guild = 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: dict[Any, Literal['track', 'album', 'artist', 'playlist', '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)
|
||||||
|
await self.update_vibe(
|
||||||
|
interaction,
|
||||||
|
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'
|
||||||
|
)
|
||||||
|
|
||||||
class ListenView(View):
|
class ListenView(View):
|
||||||
def __init__(self, item: Track | Album | Artist | Playlist | list[Track], *items: Item, timeout: float | None = 3600, disable_on_timeout: bool = False):
|
def __init__(self, item: Track | Album | Artist | Playlist | list[Track], *items: Item, timeout: float | None = 3600, disable_on_timeout: bool = False):
|
||||||
super().__init__(*items, timeout=timeout, disable_on_timeout=disable_on_timeout)
|
super().__init__(*items, timeout=timeout, disable_on_timeout=disable_on_timeout)
|
||||||
@@ -150,11 +185,13 @@ class ListenView(View):
|
|||||||
self.add_item(PlayButton(item, label="Слушать в голосовом канале", style=ButtonStyle.gray))
|
self.add_item(PlayButton(item, label="Слушать в голосовом канале", style=ButtonStyle.gray))
|
||||||
return
|
return
|
||||||
|
|
||||||
self.button1: Button = Button(label="Слушать в приложении", style=ButtonStyle.gray, url=link_app)
|
self.button1: Button = Button(label="Слушать в приложении", style=ButtonStyle.gray, url=link_app, row=0)
|
||||||
self.button2: Button = Button(label="Слушать в браузере", style=ButtonStyle.gray, url=link_web)
|
self.button2: Button = Button(label="Слушать в браузере", style=ButtonStyle.gray, url=link_web, row=0)
|
||||||
self.button3: PlayButton = PlayButton(item, label="Слушать в голосовом канале", style=ButtonStyle.gray)
|
self.button3: PlayButton = PlayButton(item, label="Слушать в голосовом канале", style=ButtonStyle.gray, row=0)
|
||||||
|
self.button4: MyVibeButton = MyVibeButton(item, label="Моя Волна", style=ButtonStyle.gray, emoji="🌊", row=1)
|
||||||
|
|
||||||
if item.available:
|
if item.available:
|
||||||
# self.add_item(self.button1) # Discord doesn't allow well formed URLs in buttons for some reason.
|
# self.add_item(self.button1) # Discord doesn't allow well formed URLs in buttons for some reason.
|
||||||
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)
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Self, cast
|
from typing import Self, cast
|
||||||
|
|
||||||
from discord.ui import View, Button, Item
|
from discord.ui import View, Button, Item, Modal, Select
|
||||||
from discord import VoiceChannel, ButtonStyle, Interaction, ApplicationContext, RawReactionActionEvent, Embed
|
from discord import VoiceChannel, ButtonStyle, Interaction, ApplicationContext, RawReactionActionEvent, Embed, ComponentType, SelectOption
|
||||||
|
|
||||||
|
import yandex_music.exceptions
|
||||||
from yandex_music import Track, ClientAsync
|
from yandex_music import Track, ClientAsync
|
||||||
from MusicBot.cogs.utils.voice_extension import VoiceExtension
|
from MusicBot.cogs.utils.voice_extension import VoiceExtension
|
||||||
|
|
||||||
@@ -123,8 +124,15 @@ class LyricsButton(Button, VoiceExtension):
|
|||||||
ClientAsync(ym_token), # type: ignore # Async client can be used here
|
ClientAsync(ym_token), # type: ignore # Async client can be used here
|
||||||
))
|
))
|
||||||
|
|
||||||
lyrics = await track.get_lyrics_async()
|
try:
|
||||||
|
lyrics = await track.get_lyrics_async()
|
||||||
|
except yandex_music.exceptions.NotFoundError:
|
||||||
|
logging.debug('Lyrics not found')
|
||||||
|
await interaction.respond("❌ Текст песни не найден. Яндекс нам соврал (опять)!", delete_after=15, ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
if not lyrics:
|
if not lyrics:
|
||||||
|
logging.debug('Lyrics not found')
|
||||||
return
|
return
|
||||||
|
|
||||||
embed = Embed(
|
embed = Embed(
|
||||||
@@ -137,6 +145,36 @@ class LyricsButton(Button, VoiceExtension):
|
|||||||
embed.add_field(name='', value=subtext, inline=False)
|
embed.add_field(name='', value=subtext, inline=False)
|
||||||
await interaction.respond(embed=embed, ephemeral=True)
|
await interaction.respond(embed=embed, ephemeral=True)
|
||||||
|
|
||||||
|
class MyVibeButton(Button, VoiceExtension):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
Button.__init__(self, **kwargs)
|
||||||
|
VoiceExtension.__init__(self, None)
|
||||||
|
|
||||||
|
async def callback(self, interaction: Interaction) -> None:
|
||||||
|
logging.info('[VIBE] My vibe button callback')
|
||||||
|
if not await self.voice_check(interaction):
|
||||||
|
return
|
||||||
|
if not interaction.guild_id:
|
||||||
|
logging.warning('[VIBE] No guild id in button callback')
|
||||||
|
return
|
||||||
|
|
||||||
|
track = self.db.get_track(interaction.guild_id, 'current')
|
||||||
|
if track:
|
||||||
|
logging.info(f"[VIBE] Playing vibe for track '{track["id"]}'")
|
||||||
|
await self.update_vibe(
|
||||||
|
interaction,
|
||||||
|
'track',
|
||||||
|
track['id'],
|
||||||
|
button_callback=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logging.info('[VIBE] Playing on your wave')
|
||||||
|
await self.update_vibe(
|
||||||
|
interaction,
|
||||||
|
'user',
|
||||||
|
'onyourwave',
|
||||||
|
button_callback=True
|
||||||
|
)
|
||||||
|
|
||||||
class MenuView(View, VoiceExtension):
|
class MenuView(View, VoiceExtension):
|
||||||
|
|
||||||
@@ -156,8 +194,9 @@ class MenuView(View, VoiceExtension):
|
|||||||
|
|
||||||
self.like_button = LikeButton(style=ButtonStyle.secondary, emoji='❤️', row=1)
|
self.like_button = LikeButton(style=ButtonStyle.secondary, emoji='❤️', row=1)
|
||||||
self.lyrics_button = LyricsButton(style=ButtonStyle.secondary, emoji='📋', row=1)
|
self.lyrics_button = LyricsButton(style=ButtonStyle.secondary, emoji='📋', row=1)
|
||||||
|
self.vibe_button = MyVibeButton(style=ButtonStyle.secondary, emoji='🌊', row=1)
|
||||||
|
|
||||||
async def init(self) -> Self:
|
async def init(self, *, disable: bool = False) -> Self:
|
||||||
current_track = self.guild['current_track']
|
current_track = self.guild['current_track']
|
||||||
likes = await self.get_likes(self.ctx)
|
likes = await self.get_likes(self.ctx)
|
||||||
|
|
||||||
@@ -177,6 +216,10 @@ class MenuView(View, VoiceExtension):
|
|||||||
|
|
||||||
self.add_item(self.like_button)
|
self.add_item(self.like_button)
|
||||||
self.add_item(self.lyrics_button)
|
self.add_item(self.lyrics_button)
|
||||||
|
self.add_item(self.vibe_button)
|
||||||
|
|
||||||
|
if disable:
|
||||||
|
self.disable_all_items()
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@@ -186,7 +229,7 @@ class MenuView(View, VoiceExtension):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if self.guild['current_menu']:
|
if self.guild['current_menu']:
|
||||||
self.db.update(self.ctx.guild_id, {'current_menu': None, 'previous_tracks': []})
|
self.db.update(self.ctx.guild_id, {'current_menu': None, 'previous_tracks': [], 'vibing': False})
|
||||||
message = await self.get_menu_message(self.ctx, self.guild['current_menu'])
|
message = await self.get_menu_message(self.ctx, self.guild['current_menu'])
|
||||||
if message:
|
if message:
|
||||||
await message.delete()
|
await message.delete()
|
||||||
|
|||||||
Reference in New Issue
Block a user