impr: Voice checks and menu fixes.

This commit is contained in:
Lemon4ksan
2025-01-10 16:19:27 +03:00
parent bcaba81f41
commit a108799e63
4 changed files with 326 additions and 173 deletions

View File

@@ -1,87 +1,80 @@
from math import ceil from math import ceil
from typing import cast from typing import cast
from io import BytesIO
import aiohttp
import discord import discord
import yandex_music from yandex_music import Track, Album, Artist, Label
from PIL import Image
from discord.ui import View, Button, Item from discord.ui import View, Button, Item
from discord import ButtonStyle, Interaction from discord import ButtonStyle, Interaction
from MusicBot.cogs.utils.voice import VoiceExtension from MusicBot.cogs.utils.voice import VoiceExtension, get_average_color_from_url
from MusicBot.database import VoiceGuildsDatabase, BaseUsersDatabase
class PlayTrackButton(Button, VoiceExtension): class PlayTrackButton(Button, VoiceExtension):
def __init__( def __init__(self, track: Track, **kwargs):
self, Button.__init__(self, **kwargs)
track: yandex_music.Track,
*,
style: ButtonStyle = ButtonStyle.secondary,
label: str | None = None,
disabled: bool = False,
custom_id: str | None = None,
url: str | None = None,
emoji: str | discord.Emoji | discord.PartialEmoji | None = None,
sku_id: int | None = None,
row: int | None = None
):
Button.__init__(self, style=style, label=label, disabled=disabled, custom_id=custom_id, url=url, emoji=emoji, sku_id=sku_id, row=row)
VoiceExtension.__init__(self) VoiceExtension.__init__(self)
self.track = track self.track = track
async def callback(self, interaction: Interaction) -> None: async def callback(self, interaction: Interaction) -> None:
if interaction.channel is None or not isinstance(interaction.channel, discord.VoiceChannel): if not interaction.guild or not await self.voice_check(interaction):
await interaction.respond("Вы должны отправить команду в голосовом канале.", ephemeral=True)
return return
title = await self.play_track(interaction, self.track) gid = interaction.guild.id
if title: guild = self.db.get_guild(gid)
await interaction.respond(f"Сейчас играет: **{title}**!", delete_after=15)
if guild['current_track'] is not None:
self.db.modify_track(gid, self.track, 'next', 'append')
if guild['current_player'] is not None and interaction.message:
await interaction.message.delete()
await interaction.respond(f"Трек **{self.track.title}** был добавлен в очередь.", delete_after=15)
else:
title = await self.play_track(interaction, self.track)
if title:
if guild['current_player'] is not None and interaction.message:
await interaction.message.delete()
await interaction.respond(f"Сейчас играет: **{title}**!", delete_after=15)
class PlayAlbumButton(Button, VoiceExtension): class PlayAlbumButton(Button, VoiceExtension):
def __init__( def __init__(self, album: Album, **kwargs):
self, Button.__init__(self, **kwargs)
album: yandex_music.Album,
*,
style: ButtonStyle = ButtonStyle.secondary,
label: str | None = None,
disabled: bool = False,
custom_id: str | None = None,
url: str | None = None,
emoji: str | discord.Emoji | discord.PartialEmoji | None = None,
sku_id: int | None = None,
row: int | None = None
):
Button.__init__(self, style=style, label=label, disabled=disabled, custom_id=custom_id, url=url, emoji=emoji, sku_id=sku_id, row=row)
VoiceExtension.__init__(self) VoiceExtension.__init__(self)
self.album = album self.album = album
async def callback(self, interaction: Interaction) -> None: async def callback(self, interaction: Interaction) -> None:
if not interaction.guild: if not interaction.guild or not await self.voice_check(interaction):
return return
gid = interaction.guild.id
guild = self.db.get_guild(gid)
album = cast(yandex_music.Album, await self.album.with_tracks_async()) album = await self.album.with_tracks_async()
if not album or not album.volumes: if not album or not album.volumes:
return return
tracks: list[Track] = []
for volume in album.volumes: for volume in album.volumes:
for track in volume: tracks.extend(volume)
self.db.add_track(interaction.guild.id, track)
if guild['current_track'] is not None:
track = self.db.pop_track(interaction.guild.id) self.db.modify_track(gid, tracks, 'next', 'extend')
ym_track = yandex_music.Track.de_json(track, client=album.client) # type: ignore if guild['current_player'] is not None and interaction.message:
title = await self.play_track(interaction, ym_track) # type: ignore await interaction.message.delete()
if title: else:
await interaction.respond(f"Сейчас играет: **{album.title}**!", delete_after=15) await interaction.respond(f"Альбом **{album.title}** был добавлен в очередь.", delete_after=15)
else: else:
await interaction.respond("Добавьте бота в голосовой канал при помощи команды /voice join.", delete_after=15, ephemeral=True) track = tracks.pop(0)
self.db.modify_track(gid, tracks, 'next', 'extend')
title = await self.play_track(interaction, track)
if title:
if guild['current_player'] is not None and interaction.message:
await interaction.message.delete()
else:
await interaction.respond(f"Сейчас играет: **{album.title}**!", delete_after=15)
class ListenTrack(View): class ListenTrack(View):
def __init__(self, track: yandex_music.Track, album_id: int, *items: Item, timeout: float | None = 360, disable_on_timeout: bool = False): def __init__(self, track: Track, album_id: int, *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)
link_app = f"yandexmusic://album/{album_id}/track/{track.id}" link_app = f"yandexmusic://album/{album_id}/track/{track.id}"
link_web = f"https://music.yandex.ru/album/{album_id}/track/{track.id}" link_web = f"https://music.yandex.ru/album/{album_id}/track/{track.id}"
@@ -94,7 +87,7 @@ class ListenTrack(View):
class ListenAlbum(View): class ListenAlbum(View):
def __init__(self, album: yandex_music.Album, *items: Item, timeout: float | None = 360, disable_on_timeout: bool = False): def __init__(self, album: Album, *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)
link_app = f"yandexmusic://album/{album.id}" link_app = f"yandexmusic://album/{album.id}"
link_web = f"https://music.yandex.ru/album/{album.id}" link_web = f"https://music.yandex.ru/album/{album.id}"
@@ -107,7 +100,7 @@ class ListenAlbum(View):
class ListenArtist(View): class ListenArtist(View):
def __init__(self, artist_id, *items: Item, timeout: float | None = 360, disable_on_timeout: bool = False): def __init__(self, artist_id: int, *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)
link_app = f"yandexmusic://artist/{artist_id}" link_app = f"yandexmusic://artist/{artist_id}"
link_web = f"https://music.yandex.ru/artist/{artist_id}" link_web = f"https://music.yandex.ru/artist/{artist_id}"
@@ -115,45 +108,9 @@ class ListenArtist(View):
self.button2 = Button(label="Слушать в браузере", style=ButtonStyle.gray, url=link_web) self.button2 = Button(label="Слушать в браузере", style=ButtonStyle.gray, url=link_web)
# 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)
async def get_average_color_from_url(url: str) -> int:
"""Get image from url and calculate its average color to use in embeds.
Args: async def proccess_album(album: Album) -> discord.Embed:
url (str): Image url.
Returns:
int: RGB Hex code. 0x000 if failed.
"""
try:
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
response.raise_for_status()
response = await response.read()
img = Image.open(BytesIO(response))
img = img.convert('RGB')
width, height = img.size
r_total, g_total, b_total = 0, 0, 0
for y in range(height):
for x in range(width):
r, g, b = cast(tuple, img.getpixel((x, y)))
r_total += r
g_total += g
b_total += b
count = width * height
r = r_total // count
g = g_total // count
b = b_total // count
return (r << 16) + (g << 8) + b
except Exception:
return 0x000
async def proccess_album(album: yandex_music.Album) -> discord.Embed:
"""Generate album embed. """Generate album embed.
Args: Args:
@@ -179,8 +136,8 @@ async def proccess_album(album: yandex_music.Album) -> discord.Embed:
cover_url = album.get_cover_url('400x400') cover_url = album.get_cover_url('400x400')
color = await get_average_color_from_url(cover_url) color = await get_average_color_from_url(cover_url)
if isinstance(album.labels[0], yandex_music.Label): if isinstance(album.labels[0], Label):
labels = [cast(yandex_music.Label, label).name for label in album.labels] labels = [cast(Label, label).name for label in album.labels]
else: else:
labels = [cast(str, label) for label in album.labels] labels = [cast(str, label) for label in album.labels]
@@ -232,7 +189,7 @@ async def proccess_album(album: yandex_music.Album) -> discord.Embed:
return embed return embed
async def process_track(track: yandex_music.Track) -> discord.Embed: async def process_track(track: Track) -> discord.Embed:
"""Generate track embed. """Generate track embed.
Args: Args:
@@ -305,7 +262,7 @@ async def process_track(track: yandex_music.Track) -> discord.Embed:
return embed return embed
async def process_artist(artist: yandex_music.Artist) -> discord.Embed: async def process_artist(artist: Artist) -> discord.Embed:
"""Generate artist embed. """Generate artist embed.
Args: Args:

View File

@@ -1,5 +1,5 @@
from discord.ui import View, Button, Item from discord.ui import View, Button, Item
from discord import ButtonStyle, Interaction, ApplicationContext from discord import InteractionMessage, ButtonStyle, Interaction, ApplicationContext
from MusicBot.cogs.utils.voice import VoiceExtension from MusicBot.cogs.utils.voice import VoiceExtension
@@ -9,14 +9,26 @@ class PlayPauseButton(Button, VoiceExtension):
VoiceExtension.__init__(self) VoiceExtension.__init__(self)
async def callback(self, interaction: Interaction) -> None: async def callback(self, interaction: Interaction) -> None:
if not await self.voice_check(interaction):
return
vc = self.get_voice_client(interaction) vc = self.get_voice_client(interaction)
if vc is not None: if vc is not None:
if not vc.is_paused(): if not vc.is_paused():
self.pause_playing(interaction) vc.pause()
await interaction.edit(content="Результат паузы.") message = interaction.message
if not message:
return
embed = message.embeds[0]
embed.set_footer(text='Приостановлено')
await interaction.edit(embed=embed)
else: else:
self.resume_playing(interaction) vc.resume()
await interaction.edit(content="Результат возобновления.") message = interaction.message
if not message:
return
embed = message.embeds[0]
embed.remove_footer()
await interaction.edit(embed=embed)
class NextTrackButton(Button, VoiceExtension): class NextTrackButton(Button, VoiceExtension):
def __init__(self, **kwargs): def __init__(self, **kwargs):
@@ -24,8 +36,11 @@ class NextTrackButton(Button, VoiceExtension):
VoiceExtension.__init__(self) VoiceExtension.__init__(self)
async def callback(self, interaction: Interaction) -> None: async def callback(self, interaction: Interaction) -> None:
await self.next_track(interaction) if not await self.voice_check(interaction):
await interaction.edit(content='Результат переключения >.') return
title = await self.next_track(interaction)
if not title:
await interaction.respond(f"Нет треков в очереди.", delete_after=15, ephemeral=True)
class PrevTrackButton(Button, VoiceExtension): class PrevTrackButton(Button, VoiceExtension):
def __init__(self, **kwargs): def __init__(self, **kwargs):
@@ -33,23 +48,27 @@ class PrevTrackButton(Button, VoiceExtension):
VoiceExtension.__init__(self) VoiceExtension.__init__(self)
async def callback(self, interaction: Interaction) -> None: async def callback(self, interaction: Interaction) -> None:
await self.prev_track(interaction) if not await self.voice_check(interaction):
await interaction.edit(content='Результат переключения <.') return
title = await self.prev_track(interaction)
if not title:
await interaction.respond(f"Нет треков в истории.", delete_after=15, ephemeral=True)
class Player(View): class Player(View):
def __init__(self, ctx: ApplicationContext, *items: Item, timeout: float | None = 3600, disable_on_timeout: bool = False): def __init__(self, ctx: ApplicationContext, *items: Item, timeout: float | None = 3600, 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)
self.ctx = ctx self.ctx = ctx
self.repeat_button = Button(style=ButtonStyle.secondary, emoji='🔂', row=0) self.repeat_button = Button(style=ButtonStyle.secondary, emoji='🔂', row=0)
self.shuffle_button = Button(style=ButtonStyle.secondary, emoji='🔀', row=0) self.shuffle_button = Button(style=ButtonStyle.secondary, emoji='🔀', row=0)
self.queue_button = Button(style=ButtonStyle.primary, emoji='📋', row=0)
self.play_pause_button = PlayPauseButton(style=ButtonStyle.primary, emoji='', row=0) self.play_pause_button = PlayPauseButton(style=ButtonStyle.primary, emoji='', row=0)
self.next_button = NextTrackButton(style=ButtonStyle.primary, emoji='', row=0) self.next_button = NextTrackButton(style=ButtonStyle.primary, emoji='', row=0)
self.prev_button = PrevTrackButton(style=ButtonStyle.primary, emoji='', row=0) self.prev_button = PrevTrackButton(style=ButtonStyle.primary, emoji='', row=0)
self.queue_button = Button(style=ButtonStyle.primary, emoji='📋', row=1)
self.add_item(self.repeat_button) self.add_item(self.repeat_button)
self.add_item(self.prev_button) self.add_item(self.prev_button)
self.add_item(self.play_pause_button) self.add_item(self.play_pause_button)

View File

@@ -1,6 +1,9 @@
import aiohttp
import asyncio import asyncio
from math import ceil
from typing import cast from typing import cast
from io import BytesIO
from PIL import Image
from yandex_music import Track, ClientAsync from yandex_music import Track, ClientAsync
@@ -9,16 +12,187 @@ from discord import Interaction, ApplicationContext
from MusicBot.database import VoiceGuildsDatabase, BaseUsersDatabase from MusicBot.database import VoiceGuildsDatabase, BaseUsersDatabase
# This should be in find.py but recursive import is a thing
async def generate_player_embed(track: Track) -> discord.Embed:
"""Generate track embed for player.
Args:
track (yandex_music.Track): Track to be processed.
Returns:
discord.Embed: Track embed.
"""
title = cast(str, track.title) # casted types are always there, blame JS for that
avail = cast(bool, track.available)
artists = track.artists_name()
albums = [cast(str, album.title) for album in track.albums]
lyrics = cast(bool, track.lyrics_available)
duration = cast(int, track.duration_ms)
explicit = track.explicit or track.content_warning
bg_video = track.background_video_uri
metadata = track.meta_data
year = track.albums[0].year
artist = track.artists[0]
cover_url = track.get_cover_url('400x400')
color = await get_average_color_from_url(cover_url)
if explicit:
title += ' <:explicit:1325879701117472869>'
duration_m = duration // 60000
duration_s = ceil(duration / 1000) - duration_m * 60
artist_url = f"https://music.yandex.ru/artist/{artist.id}"
artist_cover = artist.cover
if not artist_cover:
artist_cover_url = artist.get_op_image_url()
else:
artist_cover_url = artist_cover.get_url()
embed = discord.Embed(
title=title,
description=", ".join(albums),
color=color,
)
embed.set_thumbnail(url=cover_url)
embed.set_author(name=", ".join(artists), url=artist_url, icon_url=artist_cover_url)
embed.add_field(name="Текст песни", value="Есть" if lyrics else "Нет")
embed.add_field(name="Длительность", value=f"{duration_m}:{duration_s:02}")
if year:
embed.add_field(name="Год выпуска", value=str(year))
if metadata:
if metadata.year:
embed.add_field(name="Год выхода", value=str(metadata.year))
if metadata.number:
embed.add_field(name="Позиция", value=str(metadata.number))
if metadata.composer:
embed.add_field(name="Композитор", value=metadata.composer)
if metadata.version:
embed.add_field(name="Версия", value=metadata.version)
if bg_video:
embed.add_field(name="Видеофон", value=f"[Ссылка]({bg_video})")
if not avail:
embed.set_footer(text=f"Трек в данный момент недоступен.")
return embed
async def get_average_color_from_url(url: str) -> int:
"""Get image from url and calculate its average color to use in embeds.
Args:
url (str): Image url.
Returns:
int: RGB Hex code. 0x000 if failed.
"""
try:
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
response.raise_for_status()
response = await response.read()
img = Image.open(BytesIO(response))
img = img.convert('RGB')
width, height = img.size
r_total, g_total, b_total = 0, 0, 0
for y in range(height):
for x in range(width):
r, g, b = cast(tuple, img.getpixel((x, y)))
r_total += r
g_total += g
b_total += b
count = width * height
r = r_total // count
g = g_total // count
b = b_total // count
return (r << 16) + (g << 8) + b
except Exception:
return 0x000
class VoiceExtension: class VoiceExtension:
def __init__(self) -> None: def __init__(self) -> None:
self.db = VoiceGuildsDatabase() self.db = VoiceGuildsDatabase()
self.users_db = BaseUsersDatabase() self.users_db = BaseUsersDatabase()
def clear_queue(self, ctx: ApplicationContext | Interaction):
if ctx.guild:
self.db.update(ctx.guild.id, {'tracks_list': []})
async def update_player_embed(self, ctx: ApplicationContext | Interaction, player_mid: int) -> None:
"""Update current player message by its id.
Args:
ctx (ApplicationContext | Interaction): Context.
player_mid (int): Id of the player message. There can only be only one player in the guild.
"""
if isinstance(ctx, Interaction):
player = ctx.client.get_message(player_mid)
else:
player = await ctx.fetch_message(player_mid)
if not player:
return
guild = ctx.guild
user = ctx.user
if guild and user:
token = self.users_db.get_ym_token(user.id)
current_track = self.db.get_track(guild.id, 'current')
track = cast(Track, Track.de_json(current_track, client=ClientAsync(token))) # type: ignore
embed = await generate_player_embed(track)
if isinstance(ctx, Interaction) and ctx.message and ctx.message.id == player_mid:
# If interaction from player buttons
await ctx.edit(embed=embed)
else:
# If interaction from other buttons. They should have thair own response.
await player.edit(embed=embed)
async def voice_check(self, ctx: ApplicationContext | Interaction) -> bool:
"""Check if bot can perform voice tasks and respond if failed.
Args:
ctx (discord.ApplicationContext): Command context.
Returns:
bool: Check result.
"""
if not ctx.user:
return False
token = self.users_db.get_ym_token(ctx.user.id)
if not token:
await ctx.respond('❌ Необходимо указать свой токен доступа с помощью комманды /login.', delete_after=15, ephemeral=True)
return False
channel = ctx.channel
if not isinstance(channel, discord.VoiceChannel):
await ctx.respond("❌ Вы должны отправить команду в голосовом канале.", delete_after=15, ephemeral=True)
return False
if isinstance(ctx, Interaction):
channels = ctx.client.voice_clients
else:
channels = ctx.bot.voice_clients
voice_chat = discord.utils.get(channels, guild=ctx.guild)
if not voice_chat:
await ctx.respond("❌ Добавьте бота в голосовой канал при помощи команды /voice join.", delete_after=15, ephemeral=True)
return False
return True
def get_voice_client(self, ctx: ApplicationContext | Interaction) -> discord.VoiceClient | None: def get_voice_client(self, ctx: ApplicationContext | Interaction) -> discord.VoiceClient | None:
"""Return voice client for the given guild id. Return None if not present. """Return voice client for the given guild id. Return None if not present.
@@ -61,18 +235,19 @@ class VoiceExtension:
gid = ctx.guild.id gid = ctx.guild.id
guild = self.db.get_guild(gid) guild = self.db.get_guild(gid)
if guild.get('current_track') is not None: await track.download_async(f'music/{ctx.guild_id}.mp3')
self.db.add_track(gid, track) song = discord.FFmpegPCMAudio(f'music/{ctx.guild_id}.mp3', options='-vn -filter:a "volume=0.15"')
await ctx.respond(f"Трек **{track.title}** был добавлен в очередь.", delete_after=15)
else:
await track.download_async(f'music/{ctx.guild_id}.mp3')
song = discord.FFmpegPCMAudio(f'music/{ctx.guild_id}.mp3', options='-vn -filter:a "volume=0.15"')
vc.play(song, after=lambda exc: asyncio.run_coroutine_threadsafe(self.next_track(ctx), loop)) vc.play(song, after=lambda exc: asyncio.run_coroutine_threadsafe(self.next_track(ctx), loop))
self.db.set_current_track(gid, track) self.db.set_current_track(gid, track)
self.db.update(gid, {'is_stopped': False}) self.db.update(gid, {'is_stopped': False})
return track.title
player = guild['current_player']
if player is not None:
await self.update_player_embed(ctx, player)
return track.title
def pause_playing(self, ctx: ApplicationContext | Interaction) -> None: def pause_playing(self, ctx: ApplicationContext | Interaction) -> None:
vc = self.get_voice_client(ctx) vc = self.get_voice_client(ctx)
@@ -115,15 +290,19 @@ class VoiceExtension:
if not self.get_voice_client(ctx): # Silently return if bot got kicked if not self.get_voice_client(ctx): # Silently return if bot got kicked
return return
current_track = guild.get('current_track') current_track = guild['current_track']
tracks_list = guild.get('tracks_list') next_track = self.db.get_track(gid, 'next')
if tracks_list and current_track: if next_track and current_track:
self.db.add_previous_track(gid, current_track) self.db.modify_track(gid, current_track, 'previous', 'insert')
track = self.db.pop_track(gid) ym_track = Track.de_json(next_track, client=ClientAsync(token)) # type: ignore
ym_track = Track.de_json(track, client=ClientAsync(token)) # type: ignore self.stop_playing(ctx)
return await self.play_track(ctx, ym_track) # type: ignore
elif next_track:
ym_track = Track.de_json(next_track, client=ClientAsync(token)) # type: ignore
self.stop_playing(ctx) self.stop_playing(ctx)
return await self.play_track(ctx, ym_track) # type: ignore return await self.play_track(ctx, ym_track) # type: ignore
elif current_track: elif current_track:
self.db.modify_track(gid, current_track, 'previous', 'insert')
self.stop_playing(ctx) self.stop_playing(ctx)
async def prev_track(self, ctx: ApplicationContext | Interaction) -> str | None: async def prev_track(self, ctx: ApplicationContext | Interaction) -> str | None:
@@ -141,15 +320,14 @@ class VoiceExtension:
return return
gid = ctx.guild.id gid = ctx.guild.id
guild = self.db.get_guild(gid)
token = self.users_db.get_ym_token(ctx.user.id) token = self.users_db.get_ym_token(ctx.user.id)
current_track = guild.get('current_track') current_track = self.db.get_track(gid, 'current')
tracks_list = self.db.get_previous_tracks_list(gid) prev_track = self.db.get_track(gid, 'previous')
if tracks_list and current_track: if prev_track:
self.db.insert_track(gid, current_track) if current_track:
track = self.db.pop_previous_track(gid) self.db.modify_track(gid, current_track, 'next', 'insert')
ym_track = Track.de_json(track, client=ClientAsync(token)) # type: ignore ym_track = Track.de_json(prev_track, client=ClientAsync(token)) # type: ignore
self.stop_playing(ctx) self.stop_playing(ctx)
return await self.play_track(ctx, ym_track) # type: ignore return await self.play_track(ctx, ym_track) # type: ignore
elif current_track: elif current_track:
@@ -169,10 +347,9 @@ class VoiceExtension:
return return
gid = ctx.guild.id gid = ctx.guild.id
guild = self.db.get_guild(gid)
token = self.users_db.get_ym_token(ctx.user.id) token = self.users_db.get_ym_token(ctx.user.id)
current_track = guild.get('current_track') current_track = self.db.get_track(gid, 'current')
if current_track: if current_track:
ym_track = Track.de_json(current_track, client=ClientAsync(token)) # type: ignore ym_track = Track.de_json(current_track, client=ClientAsync(token)) # type: ignore
self.stop_playing(ctx) self.stop_playing(ctx)

View File

@@ -1,6 +1,11 @@
from typing import cast
import discord import discord
from discord.ext.commands import Cog from discord.ext.commands import Cog
from yandex_music import Track, ClientAsync
from MusicBot.cogs.utils.find import process_track
from MusicBot.cogs.utils.voice import VoiceExtension from MusicBot.cogs.utils.voice import VoiceExtension
from MusicBot.cogs.utils.player import Player from MusicBot.cogs.utils.player import Player
@@ -9,66 +14,61 @@ def setup(bot: discord.Bot):
class Voice(Cog, VoiceExtension): class Voice(Cog, VoiceExtension):
toggle = discord.SlashCommandGroup("toggle", "Команды, связанные с переключением опций.", [1247100229535141899])
voice = discord.SlashCommandGroup("voice", "Команды, связанные с голосовым каналом.", [1247100229535141899]) voice = discord.SlashCommandGroup("voice", "Команды, связанные с голосовым каналом.", [1247100229535141899])
queue = discord.SlashCommandGroup("queue", "Команды, связанные с очередью треков.", [1247100229535141899]) queue = discord.SlashCommandGroup("queue", "Команды, связанные с очередью треков.", [1247100229535141899])
track = discord.SlashCommandGroup("track", "Команды, связанные с текущим треком.", [1247100229535141899]) track = discord.SlashCommandGroup("track", "Команды, связанные с текущим треком.", [1247100229535141899])
async def voice_check(self, ctx: discord.ApplicationContext) -> bool: @voice.command(name="menu", description="Toggle player menu. Available only if you're the only one in the vocie channel.")
"""Check if bot can perform voice tasks and respond if failed.
Args:
ctx (discord.ApplicationContext): Command context.
Returns:
bool: Check result.
"""
channel = ctx.channel
if not isinstance(channel, discord.VoiceChannel):
await ctx.respond("Вы должны отправить команду в голосовом канале.", delete_after=15, ephemeral=True)
return False
channels = ctx.bot.voice_clients
voice_chat = discord.utils.get(channels, guild=ctx.guild)
if not voice_chat:
await ctx.respond("Добавьте бота в голосовой канал при помощи команды /voice join.", delete_after=15, ephemeral=True)
return False
return True
@toggle.command(name="menu", description="Toggle player menu. Available only if you're the only one in the vocie channel.")
async def menu(self, ctx: discord.ApplicationContext) -> None: async def menu(self, ctx: discord.ApplicationContext) -> None:
if self.voice_check: if not await self.voice_check(ctx):
await ctx.respond("Меню", view=Player(ctx), ephemeral=True) return
current_track = self.db.get_track(ctx.guild.id, 'current')
try:
embed = await process_track(Track.de_json(current_track, client=ClientAsync())) # type: ignore
vc = self.get_voice_client(ctx)
if not vc:
return
if not vc.is_paused():
embed.set_footer(text='Приостановлено')
else:
embed.remove_footer()
except AttributeError:
embed = None
interaction = cast(discord.Interaction, await ctx.respond(view=Player(ctx), embed=embed, delete_after=3600))
response = await interaction.original_response()
self.db.update(ctx.guild.id, {'current_player': response.id})
@voice.command(name="join", description="Join the voice channel you're currently in.") @voice.command(name="join", description="Join the voice channel you're currently in.")
async def join(self, ctx: discord.ApplicationContext) -> None: async def join(self, ctx: discord.ApplicationContext) -> None:
vc = self.get_voice_client(ctx) vc = self.get_voice_client(ctx)
if vc is not None and vc.is_playing(): if vc is not None and vc.is_playing():
await ctx.respond("Бот уже находится в голосовом канале. Выключите его с помощью команды /voice leave.", delete_after=15, ephemeral=True) await ctx.respond("Бот уже находится в голосовом канале. Выключите его с помощью команды /voice leave.", delete_after=15, ephemeral=True)
elif ctx.channel is not None and isinstance(ctx.channel, discord.VoiceChannel): elif ctx.channel is not None and isinstance(ctx.channel, discord.VoiceChannel):
await ctx.channel.connect(timeout=15) await ctx.channel.connect(timeout=15)
await ctx.respond("Подключение успешно!", delete_after=15, ephemeral=True) await ctx.respond("Подключение успешно!", delete_after=15, ephemeral=True)
else: else:
await ctx.respond("Вы должны отправить команду в голосовом канале.", delete_after=15, ephemeral=True) await ctx.respond("Вы должны отправить команду в голосовом канале.", delete_after=15, ephemeral=True)
@voice.command(description="Force the bot to leave the voice channel.") @voice.command(description="Force the bot to leave the voice channel.")
async def leave(self, ctx: discord.ApplicationContext) -> None: async def leave(self, ctx: discord.ApplicationContext) -> None:
vc = self.get_voice_client(ctx) vc = self.get_voice_client(ctx)
if await self.voice_check(ctx) and vc is not None: if await self.voice_check(ctx) and vc is not None:
self.stop_playing(ctx)
self.db.clear_history(ctx.guild.id)
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)
@queue.command(description="Clear tracks queue.") @queue.command(description="Clear tracks queue and history.")
async def clear(self, ctx: discord.ApplicationContext) -> None: async def clear(self, ctx: discord.ApplicationContext) -> None:
self.clear_queue(ctx) if not await self.voice_check(ctx):
await ctx.respond("Очередь сброшена.", delete_after=15, ephemeral=True) return
self.db.clear_history(ctx.guild.id)
await ctx.respond("Очередь и история сброшены.", delete_after=15, ephemeral=True)
@queue.command(description="Get tracks queue.") @queue.command(description="Get tracks queue.")
async def get(self, ctx: discord.ApplicationContext) -> None: async def get(self, ctx: discord.ApplicationContext) -> None:
if await self.voice_check(ctx): if await self.voice_check(ctx):
guild = self.db.get_guild(ctx.guild.id) tracks_list = self.db.get_tracks_list(ctx.guild.id, 'next')
tracks_list = guild.get('tracks_list')
embed = discord.Embed( embed = discord.Embed(
title='Список треков', title='Список треков',
color=discord.Color.dark_purple() color=discord.Color.dark_purple()
@@ -99,10 +99,10 @@ class Voice(Cog, VoiceExtension):
else: else:
await ctx.respond("Воспроизведение не на паузе.", delete_after=15, ephemeral=True) await ctx.respond("Воспроизведение не на паузе.", delete_after=15, ephemeral=True)
@track.command(description="Stop the current track and clear the queue.") @track.command(description="Stop the current track and clear the queue and history.")
async def stop(self, ctx: discord.ApplicationContext) -> None: async def stop(self, ctx: discord.ApplicationContext) -> None:
if await self.voice_check(ctx): if await self.voice_check(ctx):
self.clear_queue(ctx) self.db.clear_history(ctx.guild.id)
self.stop_playing(ctx) self.stop_playing(ctx)
await ctx.respond("Воспроизведение остановлено.", delete_after=15, ephemeral=True) await ctx.respond("Воспроизведение остановлено.", delete_after=15, ephemeral=True)
@@ -110,7 +110,7 @@ class Voice(Cog, VoiceExtension):
async def next(self, ctx: discord.ApplicationContext) -> None: async def next(self, ctx: discord.ApplicationContext) -> None:
if await self.voice_check(ctx): if await self.voice_check(ctx):
gid = ctx.guild.id gid = ctx.guild.id
tracks_list = self.db.get_tracks_list(gid) tracks_list = self.db.get_tracks_list(gid, 'next')
if not tracks_list: if not tracks_list:
await ctx.respond("Нет песенен в очереди.", delete_after=15, ephemeral=True) await ctx.respond("Нет песенен в очереди.", delete_after=15, ephemeral=True)
return return