mirror of
https://github.com/deadcxap/YandexMusicDiscordBot.git
synced 2026-01-10 09:41:46 +03:00
feat: Basic "My Vibe" implementation.
This commit is contained in:
@@ -316,7 +316,7 @@ class General(Cog):
|
|||||||
|
|
||||||
if not result:
|
if not result:
|
||||||
logging.warning(f"Failed to search for '{name}' for user {ctx.user.id}")
|
logging.warning(f"Failed to search for '{name}' for user {ctx.user.id}")
|
||||||
await ctx.respond("❌ Что-то пошло не так. Повторите попытку позже", delete_after=15, ephemeral=True)
|
await ctx.respond("❌ Что-то пошло не так. Повторите попытку позже.", delete_after=15, ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
if content_type == 'Трек':
|
if content_type == 'Трек':
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ def setup(bot):
|
|||||||
|
|
||||||
class Settings(Cog):
|
class Settings(Cog):
|
||||||
|
|
||||||
settings = discord.SlashCommandGroup("settings", "Команды для изменения настроек бота.", guild_ids=[1247100229535141899])
|
settings = discord.SlashCommandGroup("settings", "Команды для изменения настроек бота.")
|
||||||
|
|
||||||
def __init__(self, bot: discord.Bot):
|
def __init__(self, bot: discord.Bot):
|
||||||
self.db = BaseGuildsDatabase()
|
self.db = BaseGuildsDatabase()
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from PIL import Image
|
|||||||
from yandex_music import Track, Album, Artist, Playlist, Label
|
from yandex_music import Track, Album, Artist, Playlist, Label
|
||||||
from discord import Embed
|
from discord import Embed
|
||||||
|
|
||||||
async def generate_item_embed(item: Track | Album | Artist | Playlist | list[Track]) -> Embed:
|
async def generate_item_embed(item: Track | Album | Artist | Playlist | list[Track], vibing: bool = False) -> Embed:
|
||||||
"""Generate item embed.
|
"""Generate item embed.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -22,17 +22,23 @@ async def generate_item_embed(item: Track | Album | Artist | Playlist | list[Tra
|
|||||||
logging.debug(f"Generating embed for type: '{type(item).__name__}'")
|
logging.debug(f"Generating embed for type: '{type(item).__name__}'")
|
||||||
|
|
||||||
if isinstance(item, Track):
|
if isinstance(item, Track):
|
||||||
return await _generate_track_embed(item)
|
embed = await _generate_track_embed(item)
|
||||||
elif isinstance(item, Album):
|
elif isinstance(item, Album):
|
||||||
return await _generate_album_embed(item)
|
embed = await _generate_album_embed(item)
|
||||||
elif isinstance(item, Artist):
|
elif isinstance(item, Artist):
|
||||||
return await _generate_artist_embed(item)
|
embed = await _generate_artist_embed(item)
|
||||||
elif isinstance(item, Playlist):
|
elif isinstance(item, Playlist):
|
||||||
return await _generate_playlist_embed(item)
|
embed = await _generate_playlist_embed(item)
|
||||||
elif isinstance(item, list):
|
elif isinstance(item, list):
|
||||||
return _generate_likes_embed(item)
|
embed = _generate_likes_embed(item)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unknown item type: {type(item).__name__}")
|
raise ValueError(f"Unknown item type: {type(item).__name__}")
|
||||||
|
|
||||||
|
if vibing:
|
||||||
|
embed.set_image(
|
||||||
|
url="https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExbjd6M3VscnZnMXFlb3NtMHY2Zm5tbTVvMm8yY21nNXhpN214YzhyaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/7HxhnYcJljc3ON77O3/giphy.gif"
|
||||||
|
) # TODO: Get better gif
|
||||||
|
return embed
|
||||||
|
|
||||||
def _generate_likes_embed(tracks: list[Track]) -> Embed:
|
def _generate_likes_embed(tracks: list[Track]) -> Embed:
|
||||||
track_count = len(tracks)
|
track_count = len(tracks)
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Literal, cast
|
from typing import Any, Literal, cast
|
||||||
|
from time import time
|
||||||
|
|
||||||
from yandex_music import Track, TrackShort, ClientAsync
|
import yandex_music.exceptions
|
||||||
|
from yandex_music import Track, TrackShort, ClientAsync as YMClient
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord import Interaction, ApplicationContext, RawReactionActionEvent
|
from discord import Interaction, ApplicationContext, RawReactionActionEvent
|
||||||
@@ -17,22 +19,65 @@ class VoiceExtension:
|
|||||||
self.db = VoiceGuildsDatabase()
|
self.db = VoiceGuildsDatabase()
|
||||||
self.users_db = BaseUsersDatabase()
|
self.users_db = BaseUsersDatabase()
|
||||||
|
|
||||||
async def update_menu_embed(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, player_mid: int) -> bool:
|
async def send_menu_message(self, ctx: ApplicationContext | Interaction) -> None:
|
||||||
|
from MusicBot.ui import MenuView
|
||||||
|
logging.info(f"Sending player menu")
|
||||||
|
|
||||||
|
if not ctx.guild:
|
||||||
|
logging.warning("Guild not found in context inside 'create_menu'")
|
||||||
|
return
|
||||||
|
|
||||||
|
guild = self.db.get_guild(ctx.guild.id)
|
||||||
|
embed = None
|
||||||
|
|
||||||
|
if guild['current_track']:
|
||||||
|
embed = await generate_item_embed(
|
||||||
|
Track.de_json(
|
||||||
|
guild['current_track'],
|
||||||
|
client=YMClient() # type: ignore # Async client can be used here.
|
||||||
|
),
|
||||||
|
guild['vibing']
|
||||||
|
)
|
||||||
|
vc = await self.get_voice_client(ctx)
|
||||||
|
if vc and vc.is_paused():
|
||||||
|
embed.set_footer(text='Приостановлено')
|
||||||
|
else:
|
||||||
|
embed.remove_footer()
|
||||||
|
|
||||||
|
if guild['current_menu']:
|
||||||
|
logging.info(f"Deleteing old player menu {guild['current_menu']} in guild {ctx.guild.id}")
|
||||||
|
message = await self.get_menu_message(ctx, guild['current_menu'])
|
||||||
|
if message:
|
||||||
|
await message.delete()
|
||||||
|
|
||||||
|
interaction = cast(discord.Interaction, await ctx.respond(view=await MenuView(ctx).init(), embed=embed))
|
||||||
|
response = await interaction.original_response()
|
||||||
|
self.db.update(ctx.guild.id, {'current_menu': response.id})
|
||||||
|
|
||||||
|
logging.info(f"New player menu {response.id} created in guild {ctx.guild.id}")
|
||||||
|
|
||||||
|
async def update_menu_embed(
|
||||||
|
self,
|
||||||
|
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
|
||||||
|
player_mid: int,
|
||||||
|
button_callback: bool = False
|
||||||
|
) -> 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.
|
player_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.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if updated, False if not.
|
bool: True if updated, False if not.
|
||||||
"""
|
"""
|
||||||
from MusicBot.ui import MenuView
|
from MusicBot.ui import MenuView
|
||||||
logging.debug(
|
logging.debug(
|
||||||
f"Updating player embed using " +
|
f"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
|
||||||
@@ -51,29 +96,114 @@ class VoiceExtension:
|
|||||||
logging.debug(f"No token found for user {uid}")
|
logging.debug(f"No token found for user {uid}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
current_track = self.db.get_track(gid, 'current')
|
guild = self.db.get_guild(gid)
|
||||||
|
current_track = guild['current_track']
|
||||||
if not current_track:
|
if not current_track:
|
||||||
logging.debug("No current track found")
|
logging.debug("No current track found")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
track = cast(Track, Track.de_json(
|
track = cast(Track, Track.de_json(
|
||||||
current_track,
|
current_track,
|
||||||
client=ClientAsync(token) # type: ignore # Async client can be used here.
|
client=YMClient(token) # type: ignore # Async client can be used here.
|
||||||
))
|
))
|
||||||
embed = await generate_item_embed(track)
|
|
||||||
|
embed = await generate_item_embed(track, guild['vibing'])
|
||||||
|
|
||||||
if isinstance(ctx, Interaction) and ctx.message and ctx.message.id == player_mid:
|
try:
|
||||||
# If interaction from player buttons
|
if isinstance(ctx, Interaction) and button_callback:
|
||||||
await ctx.edit(embed=embed, view=await MenuView(ctx).init())
|
# If interaction from player buttons
|
||||||
else:
|
await ctx.edit(embed=embed, view=await MenuView(ctx).init())
|
||||||
# If interaction from other buttons or commands. They should have their own response.
|
else:
|
||||||
await player.edit(embed=embed, view=await MenuView(ctx).init())
|
# If interaction from other buttons or commands. They should have their own response.
|
||||||
|
await player.edit(embed=embed, view=await MenuView(ctx).init())
|
||||||
|
except discord.NotFound:
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
async def update_vibe(
|
||||||
|
self, ctx: ApplicationContext | Interaction,
|
||||||
|
type: Literal['track', 'album', 'artist', 'playlist', 'user'] | None = None,
|
||||||
|
id: str | int | None = None,
|
||||||
|
button_callback: bool = False
|
||||||
|
) -> str | None:
|
||||||
|
"""Get next vibe track. Return track title on success. If type or id is None, user's vibe will be used.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ctx (ApplicationContext | Interaction): Context.
|
||||||
|
type (Literal['track', 'album', 'artist', 'playlist', 'user'] | None, optional): Type of the item. Defaults to None.
|
||||||
|
id (str | int | Literal['onyourwave'] | None, optional): ID of the item. Defaults to None.
|
||||||
|
button_callback (bool, optional): If the function is called from button callback. Defaults to False.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str | None: Track title or None.
|
||||||
|
"""
|
||||||
|
logging.info(f"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
|
||||||
|
uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None
|
||||||
|
if not uid or not gid:
|
||||||
|
logging.warning("Guild ID or User ID not found in context inside 'vibe_update'")
|
||||||
|
return None
|
||||||
|
|
||||||
|
token = self.users_db.get_ym_token(uid)
|
||||||
|
if not token:
|
||||||
|
logging.info(f"User {uid} has no YM token")
|
||||||
|
await ctx.respond("❌ Укажите токен через /account login.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = await YMClient(token).init()
|
||||||
|
except yandex_music.exceptions.UnauthorizedError:
|
||||||
|
logging.info(f"User {uid} provided invalid token")
|
||||||
|
await ctx.respond('❌ Недействительный токен.')
|
||||||
|
return
|
||||||
|
|
||||||
|
if type and 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)
|
||||||
|
if not guild['vibing']:
|
||||||
|
feedback = await client.rotor_station_feedback_radio_started(
|
||||||
|
f"{type}:{id}",
|
||||||
|
f"desktop-user-{client.me.account.uid}", # type: ignore
|
||||||
|
timestamp=time()
|
||||||
|
)
|
||||||
|
logging.debug(f"[VIBE] Radio started feedback: {feedback}")
|
||||||
|
|
||||||
|
tracks = await client.rotor_station_tracks(
|
||||||
|
f"{type}:{id}"
|
||||||
|
)
|
||||||
|
self.db.update(gid, {'vibing': True})
|
||||||
|
elif guild['current_track']:
|
||||||
|
tracks = await client.rotor_station_tracks(
|
||||||
|
f"{type}:{id}",
|
||||||
|
queue=guild['current_track']['id']
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
tracks = None
|
||||||
|
|
||||||
|
if not tracks:
|
||||||
|
logging.warning("[VIBE] Failed to get next vibe tracks")
|
||||||
|
await ctx.respond("❌ Что-то пошло не так. Повторите попытку позже.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
logging.debug(f"[VIBE] Got next vibe tracks: {[track.track.title for track in tracks.sequence if track.track]}")
|
||||||
|
self.users_db.update(uid, {'vibe_batch_id': tracks.batch_id})
|
||||||
|
|
||||||
|
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:]]})
|
||||||
|
await self.stop_playing(ctx)
|
||||||
|
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:
|
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.
|
"""Fetch the player message by its id. Return the message if found, None if not.
|
||||||
Reset `current_player` field in the database if not found.
|
Reset `current_menu` field in the database if not found.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ctx (ApplicationContext | Interaction): Context.
|
ctx (ApplicationContext | Interaction): Context.
|
||||||
@@ -101,14 +231,14 @@ class VoiceExtension:
|
|||||||
raise ValueError(f"Invalid context type: '{type(ctx).__name__}'.")
|
raise ValueError(f"Invalid context type: '{type(ctx).__name__}'.")
|
||||||
except discord.DiscordException as e:
|
except discord.DiscordException as e:
|
||||||
logging.debug(f"Failed to get player message: {e}")
|
logging.debug(f"Failed to get player message: {e}")
|
||||||
self.db.update(ctx.guild_id, {'current_player': None})
|
self.db.update(ctx.guild_id, {'current_menu': None})
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if player:
|
if player:
|
||||||
logging.debug(f"Player message found")
|
logging.debug(f"Player message found")
|
||||||
else:
|
else:
|
||||||
logging.debug("Player message not found. Resetting current_player field.")
|
logging.debug("Player message not found. Resetting current_menu field.")
|
||||||
self.db.update(ctx.guild_id, {'current_player': None})
|
self.db.update(ctx.guild_id, {'current_menu': None})
|
||||||
|
|
||||||
return player
|
return player
|
||||||
|
|
||||||
@@ -184,7 +314,9 @@ class VoiceExtension:
|
|||||||
self,
|
self,
|
||||||
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
|
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
|
||||||
track: Track,
|
track: Track,
|
||||||
vc: discord.VoiceClient | None = None
|
*,
|
||||||
|
vc: discord.VoiceClient | None = None,
|
||||||
|
button_callback: 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.
|
||||||
@@ -193,6 +325,7 @@ 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.
|
||||||
|
button_callback (bool): Whether the interaction is a button callback.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str | None: Song title or None.
|
str | None: Song title or None.
|
||||||
@@ -220,8 +353,14 @@ class VoiceExtension:
|
|||||||
raise ValueError(f"Invalid context type: '{type(ctx).__name__}'.")
|
raise ValueError(f"Invalid context type: '{type(ctx).__name__}'.")
|
||||||
|
|
||||||
guild = self.db.get_guild(gid)
|
guild = self.db.get_guild(gid)
|
||||||
await track.download_async(f'music/{gid}.mp3')
|
try:
|
||||||
song = discord.FFmpegPCMAudio(f'music/{gid}.mp3', options='-vn -filter:a "volume=0.15"')
|
await track.download_async(f'music/{gid}.mp3')
|
||||||
|
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.
|
||||||
|
if not isinstance(ctx, RawReactionActionEvent) and ctx.user and ctx.channel:
|
||||||
|
channel = cast(discord.VoiceChannel, ctx.channel)
|
||||||
|
await channel.send(f"😔 {ctx.user.mention}, не удалось загрузить трек. Яндекс Музыка не отвечает или блокирует запросы.")
|
||||||
|
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"Playing track '{track.title}'")
|
||||||
@@ -229,9 +368,19 @@ class VoiceExtension:
|
|||||||
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})
|
||||||
|
|
||||||
player = guild['current_player']
|
player = guild['current_menu']
|
||||||
if player is not None:
|
if player is not None:
|
||||||
await self.update_menu_embed(ctx, player)
|
await self.update_menu_embed(ctx, player, button_callback)
|
||||||
|
|
||||||
|
if guild['vibing']:
|
||||||
|
user = self.users_db.get_user(uid)
|
||||||
|
feedback = await cast(YMClient, track.client).rotor_station_feedback_track_started(
|
||||||
|
f"{user['vibe_type']}:{user['vibe_id']}",
|
||||||
|
track.id,
|
||||||
|
user['vibe_batch_id'], # type: ignore # wrong typehints
|
||||||
|
time()
|
||||||
|
)
|
||||||
|
logging.debug(f"[VIBE] Track started feedback: {feedback}")
|
||||||
|
|
||||||
return track.title
|
return track.title
|
||||||
|
|
||||||
@@ -249,13 +398,13 @@ class VoiceExtension:
|
|||||||
self.db.update(gid, {'current_track': None, 'is_stopped': True})
|
self.db.update(gid, {'current_track': None, 'is_stopped': True})
|
||||||
vc.stop()
|
vc.stop()
|
||||||
|
|
||||||
|
|
||||||
async def next_track(
|
async def next_track(
|
||||||
self,
|
self,
|
||||||
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
|
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
|
||||||
vc: discord.VoiceClient | None = None,
|
vc: discord.VoiceClient | None = None,
|
||||||
*,
|
*,
|
||||||
after: bool = False
|
after: bool = False,
|
||||||
|
button_callback: bool = False
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""Switch to the next track in the queue. Return track title on success.
|
"""Switch to the next track in the queue. Return track title on success.
|
||||||
Doesn't change track if stopped. Stop playing if tracks list is empty.
|
Doesn't change track if stopped. Stop playing if tracks list is empty.
|
||||||
@@ -264,6 +413,7 @@ class VoiceExtension:
|
|||||||
ctx (ApplicationContext | Interaction): Context
|
ctx (ApplicationContext | Interaction): Context
|
||||||
vc (discord.VoiceClient, optional): Voice client.
|
vc (discord.VoiceClient, optional): Voice client.
|
||||||
after (bool, optional): Whether the function is being called by the after callback. Defaults to False.
|
after (bool, optional): Whether the function is being called by the after callback. Defaults to False.
|
||||||
|
button_interaction (bool, optional): Whether the function is being called by a button interaction. Defaults to False.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str | None: Track title or None.
|
str | None: Track title or None.
|
||||||
@@ -275,13 +425,16 @@ class VoiceExtension:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
guild = self.db.get_guild(gid)
|
guild = self.db.get_guild(gid)
|
||||||
|
user = self.users_db.get_user(uid)
|
||||||
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"No token found for user {uid}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
client = await YMClient(token).init()
|
||||||
|
|
||||||
if guild['is_stopped']:
|
if guild['is_stopped'] and after:
|
||||||
logging.debug("Playback is stopped, skipping...")
|
logging.debug("Playback is stopped, skipping after callback...")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not vc:
|
if not vc:
|
||||||
@@ -289,6 +442,28 @@ 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 guild['vibing'] and not isinstance(ctx, RawReactionActionEvent):
|
||||||
|
if guild['current_track']:
|
||||||
|
if after:
|
||||||
|
res = await client.rotor_station_feedback_track_finished(
|
||||||
|
f'{user['vibe_type']}:{user['vibe_id']}',
|
||||||
|
guild['current_track']['id'],
|
||||||
|
guild['current_track']['duration_ms'] // 1000,
|
||||||
|
user['vibe_batch_id'], # type: ignore # Wrong typehints
|
||||||
|
time()
|
||||||
|
)
|
||||||
|
logging.debug(f"[VIBE] Finished track: {res}")
|
||||||
|
else:
|
||||||
|
res = await client.rotor_station_feedback_skip(
|
||||||
|
f'{user['vibe_type']}:{user['vibe_id']}',
|
||||||
|
guild['current_track']['id'],
|
||||||
|
guild['current_track']['duration_ms'] // 1000,
|
||||||
|
user['vibe_batch_id'], # type: ignore # Wrong typehints
|
||||||
|
time()
|
||||||
|
)
|
||||||
|
logging.debug(f"[VIBE] Skipped track: {res}")
|
||||||
|
return await self.update_vibe(ctx, user['vibe_type'], user['vibe_id'], 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']
|
||||||
@@ -299,37 +474,42 @@ class VoiceExtension:
|
|||||||
logging.debug("Getting next track")
|
logging.debug("Getting next track")
|
||||||
next_track = self.db.get_track(gid, 'next')
|
next_track = self.db.get_track(gid, 'next')
|
||||||
|
|
||||||
if guild['current_track'] and guild['current_player'] and not guild['repeat']:
|
if guild['current_track'] and guild['current_menu'] and not guild['repeat']:
|
||||||
logging.debug("Adding current track to history")
|
logging.debug("Adding current track to history")
|
||||||
self.db.modify_track(gid, guild['current_track'], 'previous', 'insert')
|
self.db.modify_track(gid, guild['current_track'], 'previous', 'insert')
|
||||||
|
|
||||||
if next_track:
|
if next_track:
|
||||||
ym_track = Track.de_json(
|
ym_track = Track.de_json(
|
||||||
next_track,
|
next_track,
|
||||||
client=ClientAsync(token) # type: ignore # Async client can be used here.
|
client=client # type: ignore # Async client can be used here.
|
||||||
)
|
)
|
||||||
await self.stop_playing(ctx, vc)
|
await self.stop_playing(ctx, vc)
|
||||||
title = await self.play_track(
|
title = await self.play_track(
|
||||||
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,
|
||||||
|
button_callback=button_callback
|
||||||
)
|
)
|
||||||
|
|
||||||
if after and not guild['current_player'] and not isinstance(ctx, discord.RawReactionActionEvent):
|
if after and not guild['current_menu'] and not isinstance(ctx, discord.RawReactionActionEvent):
|
||||||
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):
|
||||||
|
logging.debug("[VIBE] No next track found, updating vibe")
|
||||||
|
return await self.update_vibe(ctx, user['vibe_type'], user['vibe_id'], 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})
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def prev_track(self, ctx: ApplicationContext | Interaction) -> str | None:
|
async def prev_track(self, ctx: ApplicationContext | Interaction, button_callback: bool = False) -> str | None:
|
||||||
"""Switch to the previous track in the queue. Repeat curren the song if no previous tracks.
|
"""Switch to the previous track in the queue. Repeat curren the song if no previous tracks.
|
||||||
Return track title on success.
|
Return track title on success.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ctx (ApplicationContext | Interaction): Context.
|
ctx (ApplicationContext | Interaction): Context.
|
||||||
|
button_callback (bool, optional): Whether the command was called by a button interaction. Defaults to False.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str | None: Track title or None.
|
str | None: Track title or None.
|
||||||
@@ -360,19 +540,19 @@ class VoiceExtension:
|
|||||||
if track:
|
if track:
|
||||||
ym_track = Track.de_json(
|
ym_track = Track.de_json(
|
||||||
track,
|
track,
|
||||||
client=ClientAsync(token) # type: ignore # Async client can be used here.
|
client=YMClient(token) # type: ignore # Async client can be used here.
|
||||||
)
|
)
|
||||||
await self.stop_playing(ctx)
|
await self.stop_playing(ctx)
|
||||||
return await self.play_track(
|
return await self.play_track(
|
||||||
ctx,
|
ctx,
|
||||||
ym_track # type: ignore # de_json should always work here.
|
ym_track, # type: ignore # de_json should always work here.
|
||||||
|
button_callback=button_callback
|
||||||
)
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
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.
|
"""Get liked tracks. Return list of tracks on success. Return None if no token found.
|
||||||
Return None if no token found.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ctx (ApplicationContext | Interaction): Context.
|
ctx (ApplicationContext | Interaction): Context.
|
||||||
@@ -389,11 +569,14 @@ class VoiceExtension:
|
|||||||
|
|
||||||
current_track = self.db.get_track(gid, 'current')
|
current_track = self.db.get_track(gid, 'current')
|
||||||
token = self.users_db.get_ym_token(uid)
|
token = self.users_db.get_ym_token(uid)
|
||||||
if not current_track or not token:
|
if not token:
|
||||||
logging.debug("Current track or token not found")
|
logging.debug(f"No token found for user {uid}")
|
||||||
|
return None
|
||||||
|
if not current_track:
|
||||||
|
logging.debug("Current track not found in 'get_likes'")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
client = await ClientAsync(token).init()
|
client = await YMClient(token).init()
|
||||||
likes = await client.users_likes_tracks()
|
likes = await client.users_likes_tracks()
|
||||||
if not likes:
|
if not likes:
|
||||||
logging.debug("No likes found")
|
logging.debug("No likes found")
|
||||||
@@ -417,10 +600,10 @@ class VoiceExtension:
|
|||||||
current_track = self.db.get_track(ctx.guild.id, 'current')
|
current_track = self.db.get_track(ctx.guild.id, 'current')
|
||||||
token = self.users_db.get_ym_token(ctx.user.id)
|
token = self.users_db.get_ym_token(ctx.user.id)
|
||||||
if not current_track or not token:
|
if not current_track or not token:
|
||||||
logging.debug("Current track or token not found")
|
logging.debug("Current track or token not found in 'like_track'")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
client = await ClientAsync(token).init()
|
client = await YMClient(token).init()
|
||||||
likes = await self.get_likes(ctx)
|
likes = await self.get_likes(ctx)
|
||||||
if not likes:
|
if not likes:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from time import time
|
||||||
from typing import cast
|
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
|
import yandex_music.exceptions
|
||||||
|
from yandex_music import ClientAsync
|
||||||
|
|
||||||
from MusicBot.cogs.utils import VoiceExtension, generate_item_embed
|
from MusicBot.cogs.utils import VoiceExtension
|
||||||
from MusicBot.ui import MenuView, 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))
|
||||||
@@ -29,7 +31,7 @@ class Voice(Cog, VoiceExtension):
|
|||||||
gid = member.guild.id
|
gid = member.guild.id
|
||||||
guild = self.db.get_guild(gid)
|
guild = self.db.get_guild(gid)
|
||||||
discord_guild = await self.typed_bot.fetch_guild(gid)
|
discord_guild = await self.typed_bot.fetch_guild(gid)
|
||||||
current_player = self.db.get_current_player(gid)
|
current_menu = self.db.get_current_menu(gid)
|
||||||
|
|
||||||
channel = after.channel or before.channel
|
channel = after.channel or before.channel
|
||||||
if not channel:
|
if not channel:
|
||||||
@@ -43,12 +45,12 @@ class Voice(Cog, VoiceExtension):
|
|||||||
self.db.update(gid, {'previous_tracks': [], 'next_tracks': [], 'current_track': None, 'is_stopped': True})
|
self.db.update(gid, {'previous_tracks': [], 'next_tracks': [], 'current_track': None, 'is_stopped': True})
|
||||||
vc.stop()
|
vc.stop()
|
||||||
elif len(channel.members) > 2 and not guild['always_allow_menu']:
|
elif len(channel.members) > 2 and not guild['always_allow_menu']:
|
||||||
if current_player:
|
if current_menu:
|
||||||
logging.info(f"Disabling current player for guild {gid} due to multiple members")
|
logging.info(f"Disabling current player for guild {gid} due to multiple members")
|
||||||
|
|
||||||
self.db.update(gid, {'current_player': None, 'repeat': False, 'shuffle': False})
|
self.db.update(gid, {'current_menu': None, 'repeat': False, 'shuffle': False})
|
||||||
try:
|
try:
|
||||||
message = await channel.fetch_message(current_player)
|
message = await channel.fetch_message(current_menu)
|
||||||
await message.delete()
|
await message.delete()
|
||||||
await channel.send("Меню отключено из-за большого количества участников.", delete_after=15)
|
await channel.send("Меню отключено из-за большого количества участников.", delete_after=15)
|
||||||
except (discord.NotFound, discord.Forbidden):
|
except (discord.NotFound, discord.Forbidden):
|
||||||
@@ -191,42 +193,16 @@ class Voice(Cog, VoiceExtension):
|
|||||||
@voice.command(name="menu", description="Создать меню проигрывателя. Доступно только если вы единственный в голосовом канале.")
|
@voice.command(name="menu", description="Создать меню проигрывателя. Доступно только если вы единственный в голосовом канале.")
|
||||||
async def menu(self, ctx: discord.ApplicationContext) -> None:
|
async def menu(self, ctx: discord.ApplicationContext) -> None:
|
||||||
logging.info(f"Menu command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
|
logging.info(f"Menu command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
|
||||||
if not await self.voice_check(ctx):
|
|
||||||
return
|
|
||||||
|
|
||||||
guild = self.db.get_guild(ctx.guild.id)
|
guild = self.db.get_guild(ctx.guild.id)
|
||||||
channel = cast(discord.VoiceChannel, ctx.channel)
|
channel = cast(discord.VoiceChannel, ctx.channel)
|
||||||
embed = None
|
|
||||||
|
|
||||||
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"Action declined: other members are present in the voice channel")
|
logging.info(f"Action declined: other members are present in the voice channel")
|
||||||
await ctx.respond("❌ Вы не единственный в голосовом канале.", ephemeral=True)
|
await ctx.respond("❌ Вы не единственный в голосовом канале.", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
if guild['current_track']:
|
await self.send_menu_message(ctx)
|
||||||
embed = await generate_item_embed(
|
|
||||||
Track.de_json(
|
|
||||||
guild['current_track'],
|
|
||||||
client=ClientAsync() # type: ignore # Async client can be used here.
|
|
||||||
)
|
|
||||||
)
|
|
||||||
vc = await self.get_voice_client(ctx)
|
|
||||||
if vc and vc.is_paused():
|
|
||||||
embed.set_footer(text='Приостановлено')
|
|
||||||
else:
|
|
||||||
embed.remove_footer()
|
|
||||||
|
|
||||||
if guild['current_player']:
|
|
||||||
logging.info(f"Deleteing old player menu {guild['current_player']} in guild {ctx.guild.id}")
|
|
||||||
message = await self.get_menu_message(ctx, guild['current_player'])
|
|
||||||
if message:
|
|
||||||
await message.delete()
|
|
||||||
|
|
||||||
interaction = cast(discord.Interaction, await ctx.respond(view=await MenuView(ctx).init(), embed=embed, delete_after=3600))
|
|
||||||
response = await interaction.original_response()
|
|
||||||
self.db.update(ctx.guild.id, {'current_player': response.id})
|
|
||||||
|
|
||||||
logging.info(f"New player menu {response.id} created in guild {ctx.guild.id}")
|
|
||||||
|
|
||||||
@voice.command(name="join", description="Подключиться к голосовому каналу, в котором вы сейчас находитесь.")
|
@voice.command(name="join", description="Подключиться к голосовому каналу, в котором вы сейчас находитесь.")
|
||||||
async def join(self, ctx: discord.ApplicationContext) -> None:
|
async def join(self, ctx: discord.ApplicationContext) -> None:
|
||||||
@@ -308,7 +284,7 @@ class Voice(Cog, VoiceExtension):
|
|||||||
if not vc.is_paused():
|
if not vc.is_paused():
|
||||||
vc.pause()
|
vc.pause()
|
||||||
|
|
||||||
player = self.db.get_current_player(ctx.guild.id)
|
player = self.db.get_current_menu(ctx.guild.id)
|
||||||
if player:
|
if player:
|
||||||
await self.update_menu_embed(ctx, player)
|
await self.update_menu_embed(ctx, player)
|
||||||
|
|
||||||
@@ -332,7 +308,7 @@ class Voice(Cog, VoiceExtension):
|
|||||||
elif await self.voice_check(ctx) and (vc := await self.get_voice_client(ctx)):
|
elif await self.voice_check(ctx) and (vc := await self.get_voice_client(ctx)):
|
||||||
if vc.is_paused():
|
if vc.is_paused():
|
||||||
vc.resume()
|
vc.resume()
|
||||||
player = self.db.get_current_player(ctx.guild.id)
|
player = self.db.get_current_menu(ctx.guild.id)
|
||||||
if player:
|
if player:
|
||||||
await self.update_menu_embed(ctx, player)
|
await self.update_menu_embed(ctx, player)
|
||||||
logging.info(f"Track resumed in guild {ctx.guild.id}")
|
logging.info(f"Track resumed in guild {ctx.guild.id}")
|
||||||
@@ -353,18 +329,48 @@ class Voice(Cog, VoiceExtension):
|
|||||||
await ctx.respond("❌ Вы не можете остановить воспроизведение, пока в канале находятся другие пользователи.", delete_after=15, ephemeral=True)
|
await ctx.respond("❌ Вы не можете остановить воспроизведение, пока в канале находятся другие пользователи.", delete_after=15, ephemeral=True)
|
||||||
|
|
||||||
elif await self.voice_check(ctx):
|
elif await self.voice_check(ctx):
|
||||||
self.db.update(ctx.guild.id, {'previous_tracks': [], 'next_tracks': []})
|
|
||||||
await self.stop_playing(ctx)
|
await self.stop_playing(ctx)
|
||||||
|
|
||||||
current_player = self.db.get_current_player(ctx.guild.id)
|
current_menu = self.db.get_current_menu(ctx.guild.id)
|
||||||
if current_player:
|
if current_menu:
|
||||||
player = await self.get_menu_message(ctx, current_player)
|
player = await self.get_menu_message(ctx, current_menu)
|
||||||
if player:
|
if player:
|
||||||
await player.delete()
|
await player.delete()
|
||||||
|
|
||||||
|
self.db.update(ctx.guild.id, {
|
||||||
|
'current_menu': None, 'repeat': False, 'shuffle': False, 'previous_tracks': [], 'next_tracks': [], 'vibing': False
|
||||||
|
})
|
||||||
logging.info(f"Playback stopped in guild {ctx.guild.id}")
|
logging.info(f"Playback stopped in guild {ctx.guild.id}")
|
||||||
|
|
||||||
|
guild = self.db.get_guild(ctx.guild_id)
|
||||||
|
if guild['vibing']:
|
||||||
|
user = self.users_db.get_user(ctx.user.id)
|
||||||
|
token = user['ym_token']
|
||||||
|
if not token:
|
||||||
|
logging.info(f"User {ctx.user.id} has no YM token")
|
||||||
|
await ctx.respond("❌ Укажите токен через /account login.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
self.db.update(ctx.guild.id, {'current_player': None, 'repeat': False, 'shuffle': False})
|
try:
|
||||||
|
client = await ClientAsync(token).init()
|
||||||
|
except yandex_music.exceptions.UnauthorizedError:
|
||||||
|
logging.info(f"User {ctx.user.id} provided invalid token")
|
||||||
|
await ctx.respond('❌ Недействительный токен.')
|
||||||
|
return
|
||||||
|
|
||||||
|
track = guild['current_track']
|
||||||
|
if not track:
|
||||||
|
return
|
||||||
|
|
||||||
|
res = await client.rotor_station_feedback_track_finished(
|
||||||
|
f"{user['vibe_type']}:{user['vibe_id']}",
|
||||||
|
track['id'],
|
||||||
|
track['duration_ms'] // 1000,
|
||||||
|
cast(str, user['vibe_batch_id']),
|
||||||
|
time()
|
||||||
|
)
|
||||||
|
logging.info(f"User {ctx.user.id} finished vibing with result: {res}")
|
||||||
|
|
||||||
await ctx.respond("Воспроизведение остановлено.", delete_after=15, ephemeral=True)
|
await ctx.respond("Воспроизведение остановлено.", delete_after=15, ephemeral=True)
|
||||||
|
|
||||||
@track.command(description="Переключиться на следующую песню в очереди.")
|
@track.command(description="Переключиться на следующую песню в очереди.")
|
||||||
@@ -433,3 +439,24 @@ class Voice(Cog, VoiceExtension):
|
|||||||
else:
|
else:
|
||||||
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="Запустить мою волну по текущему треку.")
|
||||||
|
async def vibe(self, ctx: discord.ApplicationContext) -> None:
|
||||||
|
logging.info(f"Vibe command invoked by user {ctx.author.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
|
||||||
|
if not guild['current_track']:
|
||||||
|
logging.info(f"No current track in {ctx.guild.id}")
|
||||||
|
await ctx.respond("❌ Нет воспроизводимого трека.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.send_menu_message(ctx)
|
||||||
|
await self.update_vibe(ctx, 'track', guild['current_track']['id'])
|
||||||
|
|||||||
@@ -26,7 +26,10 @@ class BaseUsersDatabase:
|
|||||||
ym_token=None,
|
ym_token=None,
|
||||||
playlists=[],
|
playlists=[],
|
||||||
playlists_page=0,
|
playlists_page=0,
|
||||||
queue_page=0
|
queue_page=0,
|
||||||
|
vibe_batch_id=None,
|
||||||
|
vibe_type=None,
|
||||||
|
vibe_id=None
|
||||||
))
|
))
|
||||||
|
|
||||||
def update(self, uid: int, data: User) -> None:
|
def update(self, uid: int, data: User) -> None:
|
||||||
@@ -54,14 +57,18 @@ class BaseUsersDatabase:
|
|||||||
user = users.find_one({'_id': uid})
|
user = users.find_one({'_id': uid})
|
||||||
user = cast(ExplicitUser, user)
|
user = cast(ExplicitUser, user)
|
||||||
existing_fields = user.keys()
|
existing_fields = user.keys()
|
||||||
fields: User = User(
|
fields: ExplicitUser = ExplicitUser(
|
||||||
|
_id=0,
|
||||||
ym_token=None,
|
ym_token=None,
|
||||||
playlists=[],
|
playlists=[],
|
||||||
playlists_page=0,
|
playlists_page=0,
|
||||||
queue_page=0
|
queue_page=0,
|
||||||
|
vibe_batch_id=None,
|
||||||
|
vibe_type=None,
|
||||||
|
vibe_id=None
|
||||||
)
|
)
|
||||||
for field, default_value in fields.items():
|
for field, default_value in fields.items():
|
||||||
if field not in existing_fields and field != '_id':
|
if field not in existing_fields:
|
||||||
user[field] = default_value
|
user[field] = default_value
|
||||||
users.update_one({'_id': uid}, {"$set": {field: default_value}})
|
users.update_one({'_id': uid}, {"$set": {field: default_value}})
|
||||||
|
|
||||||
@@ -87,7 +94,7 @@ class BaseGuildsDatabase:
|
|||||||
next_tracks=[],
|
next_tracks=[],
|
||||||
previous_tracks=[],
|
previous_tracks=[],
|
||||||
current_track=None,
|
current_track=None,
|
||||||
current_player=None,
|
current_menu=None,
|
||||||
is_stopped=True,
|
is_stopped=True,
|
||||||
allow_explicit=True,
|
allow_explicit=True,
|
||||||
always_allow_menu=False,
|
always_allow_menu=False,
|
||||||
@@ -98,7 +105,8 @@ class BaseGuildsDatabase:
|
|||||||
vote_add_playlist=True,
|
vote_add_playlist=True,
|
||||||
shuffle=False,
|
shuffle=False,
|
||||||
repeat=False,
|
repeat=False,
|
||||||
votes={}
|
votes={},
|
||||||
|
vibing=False
|
||||||
))
|
))
|
||||||
|
|
||||||
def update(self, gid: int, data: Guild) -> None:
|
def update(self, gid: int, data: Guild) -> None:
|
||||||
@@ -127,11 +135,12 @@ class BaseGuildsDatabase:
|
|||||||
|
|
||||||
guild = cast(ExplicitGuild, guild)
|
guild = cast(ExplicitGuild, guild)
|
||||||
existing_fields = guild.keys()
|
existing_fields = guild.keys()
|
||||||
fields = Guild(
|
fields = ExplicitGuild(
|
||||||
|
_id=0,
|
||||||
next_tracks=[],
|
next_tracks=[],
|
||||||
previous_tracks=[],
|
previous_tracks=[],
|
||||||
current_track=None,
|
current_track=None,
|
||||||
current_player=None,
|
current_menu=None,
|
||||||
is_stopped=True,
|
is_stopped=True,
|
||||||
allow_explicit=True,
|
allow_explicit=True,
|
||||||
always_allow_menu=False,
|
always_allow_menu=False,
|
||||||
@@ -142,10 +151,11 @@ class BaseGuildsDatabase:
|
|||||||
vote_add_playlist=True,
|
vote_add_playlist=True,
|
||||||
shuffle=False,
|
shuffle=False,
|
||||||
repeat=False,
|
repeat=False,
|
||||||
votes={}
|
votes={},
|
||||||
|
vibing=False
|
||||||
)
|
)
|
||||||
for field, default_value in fields.items():
|
for field, default_value in fields.items():
|
||||||
if field not in existing_fields and field != '_id':
|
if field not in existing_fields:
|
||||||
guild[field] = default_value
|
guild[field] = default_value
|
||||||
guilds.update_one({'_id': gid}, {"$set": {field: default_value}})
|
guilds.update_one({'_id': gid}, {"$set": {field: default_value}})
|
||||||
|
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ class VoiceGuildsDatabase(BaseGuildsDatabase):
|
|||||||
track = track.to_dict()
|
track = track.to_dict()
|
||||||
self.update(gid, {'current_track': track})
|
self.update(gid, {'current_track': track})
|
||||||
|
|
||||||
def get_current_player(self, gid: int) -> int | None:
|
def get_current_menu(self, gid: int) -> int | None:
|
||||||
"""Get current player.
|
"""Get current player.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -153,4 +153,4 @@ class VoiceGuildsDatabase(BaseGuildsDatabase):
|
|||||||
Returns: int | None: Player message id or None if not present.
|
Returns: int | None: Player message id or None if not present.
|
||||||
"""
|
"""
|
||||||
guild = self.get_guild(gid)
|
guild = self.get_guild(gid)
|
||||||
return guild['current_player']
|
return guild['current_menu']
|
||||||
@@ -11,7 +11,7 @@ class Guild(TypedDict, total=False):
|
|||||||
next_tracks: list[dict[str, Any]]
|
next_tracks: list[dict[str, Any]]
|
||||||
previous_tracks: list[dict[str, Any]]
|
previous_tracks: list[dict[str, Any]]
|
||||||
current_track: dict[str, Any] | None
|
current_track: dict[str, Any] | None
|
||||||
current_player: int | None
|
current_menu: int | None
|
||||||
is_stopped: bool
|
is_stopped: bool
|
||||||
allow_explicit: bool
|
allow_explicit: bool
|
||||||
always_allow_menu: bool
|
always_allow_menu: bool
|
||||||
@@ -23,13 +23,14 @@ class Guild(TypedDict, total=False):
|
|||||||
shuffle: bool
|
shuffle: bool
|
||||||
repeat: bool
|
repeat: bool
|
||||||
votes: dict[str, MessageVotes]
|
votes: dict[str, MessageVotes]
|
||||||
|
vibing: bool
|
||||||
|
|
||||||
class ExplicitGuild(TypedDict):
|
class ExplicitGuild(TypedDict):
|
||||||
_id: int
|
_id: int
|
||||||
next_tracks: list[dict[str, Any]]
|
next_tracks: list[dict[str, Any]]
|
||||||
previous_tracks: list[dict[str, Any]]
|
previous_tracks: list[dict[str, Any]]
|
||||||
current_track: dict[str, Any] | None
|
current_track: dict[str, Any] | None
|
||||||
current_player: int | None
|
current_menu: int | None
|
||||||
is_stopped: bool # Prevents the `after` callback of play_track
|
is_stopped: bool # Prevents the `after` callback of play_track
|
||||||
allow_explicit: bool
|
allow_explicit: bool
|
||||||
always_allow_menu: bool
|
always_allow_menu: bool
|
||||||
@@ -40,4 +41,5 @@ class ExplicitGuild(TypedDict):
|
|||||||
vote_add_playlist: bool
|
vote_add_playlist: bool
|
||||||
shuffle: bool
|
shuffle: bool
|
||||||
repeat: bool
|
repeat: bool
|
||||||
votes: dict[str, MessageVotes]
|
votes: dict[str, MessageVotes]
|
||||||
|
vibing: bool
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
from typing import TypedDict
|
from typing import TypedDict, Literal
|
||||||
|
|
||||||
class User(TypedDict, total=False):
|
class User(TypedDict, total=False):
|
||||||
ym_token: str | None
|
ym_token: str | None
|
||||||
playlists: list[tuple[str, int]]
|
playlists: list[tuple[str, int]]
|
||||||
playlists_page: int
|
playlists_page: int
|
||||||
queue_page: int
|
queue_page: int
|
||||||
|
vibe_batch_id: str | None
|
||||||
|
vibe_type: Literal['track', 'album', 'artist', 'playlist', 'user'] | None
|
||||||
|
vibe_id: str | int | None
|
||||||
|
|
||||||
class ExplicitUser(TypedDict):
|
class ExplicitUser(TypedDict):
|
||||||
_id: int
|
_id: int
|
||||||
@@ -12,3 +15,6 @@ class ExplicitUser(TypedDict):
|
|||||||
playlists: list[tuple[str, int]] # name / tracks count
|
playlists: list[tuple[str, int]] # name / tracks count
|
||||||
playlists_page: int
|
playlists_page: int
|
||||||
queue_page: int
|
queue_page: int
|
||||||
|
vibe_batch_id: str | None
|
||||||
|
vibe_type: Literal['track', 'album', 'artist', 'playlist', 'user'] | None
|
||||||
|
vibe_id: str | int | None
|
||||||
|
|||||||
@@ -119,12 +119,12 @@ class PlayButton(Button, VoiceExtension):
|
|||||||
await self.play_track(interaction, track)
|
await self.play_track(interaction, track)
|
||||||
response_message = f"Сейчас играет: **{track.title}**!"
|
response_message = f"Сейчас играет: **{track.title}**!"
|
||||||
|
|
||||||
current_player = None
|
current_menu = None
|
||||||
if guild['current_player']:
|
if guild['current_menu']:
|
||||||
current_player = await self.get_menu_message(interaction, guild['current_player'])
|
current_menu = await self.get_menu_message(interaction, guild['current_menu'])
|
||||||
|
|
||||||
if current_player and interaction.message:
|
if current_menu and interaction.message:
|
||||||
logging.debug(f"Deleting interaction message {interaction.message.id}: current player {current_player.id} found")
|
logging.debug(f"Deleting interaction message {interaction.message.id}: current player {current_menu.id} found")
|
||||||
await interaction.message.delete()
|
await interaction.message.delete()
|
||||||
else:
|
else:
|
||||||
await interaction.respond(response_message, delete_after=15)
|
await interaction.respond(response_message, delete_after=15)
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class NextTrackButton(Button, VoiceExtension):
|
|||||||
logging.info('Next track button callback...')
|
logging.info('Next track button callback...')
|
||||||
if not await self.voice_check(interaction):
|
if not await self.voice_check(interaction):
|
||||||
return
|
return
|
||||||
title = await self.next_track(interaction)
|
title = await self.next_track(interaction, button_callback=True)
|
||||||
if not title:
|
if not title:
|
||||||
await interaction.respond(f"Нет треков в очереди.", delete_after=15, ephemeral=True)
|
await interaction.respond(f"Нет треков в очереди.", delete_after=15, ephemeral=True)
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ class PrevTrackButton(Button, VoiceExtension):
|
|||||||
logging.info('Previous track button callback...')
|
logging.info('Previous track button callback...')
|
||||||
if not await self.voice_check(interaction):
|
if not await self.voice_check(interaction):
|
||||||
return
|
return
|
||||||
title = await self.prev_track(interaction)
|
title = await self.prev_track(interaction, button_callback=True)
|
||||||
if not title:
|
if not title:
|
||||||
await interaction.respond(f"Нет треков в истории.", delete_after=15, ephemeral=True)
|
await interaction.respond(f"Нет треков в истории.", delete_after=15, ephemeral=True)
|
||||||
|
|
||||||
@@ -185,9 +185,9 @@ class MenuView(View, VoiceExtension):
|
|||||||
if not self.ctx.guild_id:
|
if not self.ctx.guild_id:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.guild['current_player']:
|
if self.guild['current_menu']:
|
||||||
self.db.update(self.ctx.guild_id, {'current_player': None, 'previous_tracks': []})
|
self.db.update(self.ctx.guild_id, {'current_menu': None, 'previous_tracks': []})
|
||||||
message = await self.get_menu_message(self.ctx, self.guild['current_player'])
|
message = await self.get_menu_message(self.ctx, self.guild['current_menu'])
|
||||||
if message:
|
if message:
|
||||||
await message.delete()
|
await message.delete()
|
||||||
logging.debug('Successfully deleted menu message')
|
logging.debug('Successfully deleted menu message')
|
||||||
|
|||||||
Reference in New Issue
Block a user