feat: Add to playlist and dislike button.

This commit is contained in:
Lemon4ksan
2025-02-01 22:20:22 +03:00
parent 956dc4d925
commit a74bf152c0
5 changed files with 214 additions and 62 deletions

View File

@@ -19,7 +19,7 @@ def setup(bot):
bot.add_cog(General(bot)) bot.add_cog(General(bot))
async def get_search_suggestions(ctx: discord.AutocompleteContext) -> list[str]: async def get_search_suggestions(ctx: discord.AutocompleteContext) -> list[str]:
if not ctx.interaction.user or not ctx.value: if not ctx.interaction.user or not ctx.value or len(ctx.value) < 2:
return [] return []
users_db = BaseUsersDatabase() users_db = BaseUsersDatabase()
@@ -93,10 +93,11 @@ class General(Cog):
if command == 'all': if command == 'all':
embed.description = ( embed.description = (
"Этот бот позволяет слушать музыку из вашего аккаунта Yandex Music.\n" "Этот бот позволяет слушать музыку из вашего аккаунта Яндекс Музыки.\n"
"Зарегистрируйте свой токен с помощью /login. Его можно получить [здесь](https://github.com/MarshalX/yandex-music-api/discussions/513).\n" "Зарегистрируйте свой токен с помощью /login. Его можно получить [здесь](https://github.com/MarshalX/yandex-music-api/discussions/513).\n"
"Для получения помощи по конкретной команде, введите /help <команда>.\n" "Для получения помощи по конкретной команде, введите /help <команда>.\n"
"Для изменения настроек необходимо иметь права управления каналами на сервере.\n\n" "Для изменения настроек необходимо иметь права управления каналами на сервере.\n\n"
"Помните, что это **не замена Яндекс Музыки**, а лишь её дополнение. Не ожидайте безупречного звука.\n\n"
"**Для дополнительной помощи, присоединяйтесь к [серверу любителей Яндекс Музыки](https://discord.gg/gkmFDaPMeC).**" "**Для дополнительной помощи, присоединяйтесь к [серверу любителей Яндекс Музыки](https://discord.gg/gkmFDaPMeC).**"
) )

View File

@@ -1,5 +1,7 @@
import asyncio import asyncio
import aiofiles
import logging import logging
import io
from typing import Any, Literal, cast from typing import Any, Literal, cast
from time import time from time import time
@@ -228,6 +230,16 @@ class VoiceExtension:
logging.debug(f"[VIBE] Radio started feedback: {feedback}") logging.debug(f"[VIBE] Radio started feedback: {feedback}")
tracks = await client.rotor_station_tracks(f"{type}:{id}") tracks = await client.rotor_station_tracks(f"{type}:{id}")
self.db.update(gid, {'vibing': True}) self.db.update(gid, {'vibing': True})
if update_settings:
settings = user['vibe_settings']
await client.rotor_station_settings2(
f"{type}:{id}",
mood_energy=settings['mood'],
diversity=settings['diversity'],
language=settings['lang']
)
elif guild['current_track']: elif guild['current_track']:
if update_settings: if update_settings:
settings = user['vibe_settings'] settings = user['vibe_settings']
@@ -404,7 +416,12 @@ class VoiceExtension:
await channel.send(f"😔 Не удалось загрузить трек. Попробуйте сбросить меню.", delete_after=15) await channel.send(f"😔 Не удалось загрузить трек. Попробуйте сбросить меню.", delete_after=15)
return None return None
song = discord.FFmpegPCMAudio(f'music/{gid}.mp3', options='-vn -filter:a "volume=0.15"') async with aiofiles.open(f'music/{gid}.mp3', "rb") as f:
track_bytes = io.BytesIO(await f.read())
song = discord.FFmpegPCMAudio(track_bytes, pipe=True, options='-vn -filter:a "volume=0.15"')
if not guild['current_menu']:
await asyncio.sleep(1)
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"[VC_EXT] Playing track '{track.title}'") logging.info(f"[VC_EXT] Playing track '{track.title}'")
@@ -422,13 +439,25 @@ class VoiceExtension:
return track.title return track.title
async def stop_playing(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, vc: discord.VoiceClient | None = None) -> None: async def stop_playing(
self, ctx: ApplicationContext | Interaction | RawReactionActionEvent,
*,
vc: discord.VoiceClient | None = None,
full: bool = False
) -> 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
if not gid: uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None
if not gid or not uid:
logging.warning("[VC_EXT] Guild ID not found in context") logging.warning("[VC_EXT] Guild ID not found in context")
return return
guild = self.db.get_guild(gid)
if gid in menu_views:
menu_views[gid].stop()
del menu_views[gid]
if not vc: if not vc:
vc = await self.get_voice_client(ctx) vc = await self.get_voice_client(ctx)
if vc: if vc:
@@ -436,6 +465,49 @@ 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()
if full:
if guild['current_menu']:
menu = await self.get_menu_message(ctx, guild['current_menu'])
if menu:
await menu.delete()
self.db.update(gid, {
'current_menu': None, 'repeat': False, 'shuffle': False, 'previous_tracks': [], 'next_tracks': [], 'vibing': False
})
logging.info(f"[VOICE] Playback stopped in guild {gid}")
if guild['vibing']:
user = self.users_db.get_user(uid)
token = user['ym_token']
if not token:
logging.info(f"[VOICE] User {uid} has no YM token")
if not isinstance(ctx, RawReactionActionEvent):
await ctx.respond("❌ Укажите токен через /account login.", delete_after=15, ephemeral=True)
return
client = await self.init_ym_client(ctx, user['ym_token'])
if not client:
logging.info(f"[VOICE] Failed to init YM client for user {uid}")
if not isinstance(ctx, RawReactionActionEvent):
await ctx.respond("❌ Что-то пошло не так. Попробуйте позже.", delete_after=15, ephemeral=True)
return
track = guild['current_track']
if not track:
logging.info(f"[VOICE] No current track in guild {gid}")
if not isinstance(ctx, RawReactionActionEvent):
await ctx.respond("❌ Что-то пошло не так. Попробуйте позже.", delete_after=15, ephemeral=True)
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"[VOICE] User {uid} finished vibing with result: {res}")
async def next_track( async def next_track(
self, self,
ctx: ApplicationContext | Interaction | RawReactionActionEvent, ctx: ApplicationContext | Interaction | RawReactionActionEvent,
@@ -543,7 +615,7 @@ class VoiceExtension:
next_track, next_track,
client=client # type: ignore # Async client can be used here. client=client # type: ignore # Async client can be used here.
) )
await self.stop_playing(ctx, vc) await self.stop_playing(ctx, vc=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.
@@ -696,6 +768,34 @@ class VoiceExtension:
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 dislike_track(self, ctx: ApplicationContext | Interaction) -> bool:
"""Dislike current track. Return track title on success.
Args:
ctx (ApplicationContext | Interaction): Context.
Returns:
str | None: Track title or None.
"""
if not ctx.guild or not ctx.user:
logging.warning("[VC_EXT] Guild or User not found in context inside 'dislike_track'")
return False
current_track = self.db.get_track(ctx.guild.id, 'current')
if not current_track:
logging.debug("[VC_EXT] Current track not found in 'dislike_track'")
return False
client = await self.init_ym_client(ctx)
if not client:
return False
res = await client.users_dislikes_tracks_add(
current_track['id'],
client.me.account.uid # type: ignore
)
return res
async def _retry_update_menu_embed( async def _retry_update_menu_embed(
self, self,
ctx: ApplicationContext | Interaction, ctx: ApplicationContext | Interaction,

View File

@@ -1,5 +1,4 @@
import logging import logging
from time import time
from typing import cast from typing import cast
import discord import discord
@@ -241,11 +240,8 @@ class Voice(Cog, VoiceExtension):
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) if await self.voice_check(ctx):
if await self.voice_check(ctx) and vc: await self.stop_playing(ctx, full=True)
self.db.update(ctx.guild.id, {'previous_tracks': [], 'next_tracks': [], 'current_track': None, 'is_stopped': True})
vc.stop()
await vc.disconnect(force=True)
await ctx.respond("Отключение успешно!", delete_after=15, ephemeral=True) await ctx.respond("Отключение успешно!", delete_after=15, ephemeral=True)
logging.info(f"[VOICE] Successfully disconnected from voice channel in guild {ctx.guild.id}") logging.info(f"[VOICE] Successfully disconnected from voice channel in guild {ctx.guild.id}")
@@ -338,51 +334,7 @@ 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):
guild = self.db.get_guild(ctx.guild.id) await self.stop_playing(ctx, full=True)
await self.stop_playing(ctx)
if guild['current_menu']:
menu = await self.get_menu_message(ctx, guild['current_menu'])
if menu:
await menu.delete()
self.db.update(ctx.guild.id, {
'current_menu': None, 'repeat': False, 'shuffle': False, 'previous_tracks': [], 'next_tracks': [], 'vibing': False
})
logging.info(f"[VOICE] Playback stopped in 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"[VOICE] User {ctx.user.id} has no YM token")
await ctx.respond("❌ Укажите токен через /account login.", delete_after=15, ephemeral=True)
return
client = await self.init_ym_client(ctx, user['ym_token'])
if not client:
logging.info(f"[VOICE] Failed to init YM client for user {ctx.user.id}")
await ctx.respond("❌ Что-то пошло не так. Попробуйте позже.", delete_after=15, ephemeral=True)
return
track = guild['current_track']
if not track:
logging.info(f"[VOICE] No current track in guild {ctx.guild.id}")
await ctx.respond("❌ Что-то пошло не так. Попробуйте позже.", delete_after=15, ephemeral=True)
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"[VOICE] User {ctx.user.id} finished vibing with result: {res}")
if ctx.guild.id in menu_views:
menu_views[ctx.guild.id].stop()
del menu_views[ctx.guild.id]
await ctx.respond("Воспроизведение остановлено.", delete_after=15, ephemeral=True) await ctx.respond("Воспроизведение остановлено.", delete_after=15, ephemeral=True)
@@ -453,7 +405,7 @@ class Voice(Cog, VoiceExtension):
logging.info(f"[VOICE] Track added to favorites for user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"[VOICE] 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(name='vibe', description="Запустить мою волну по текущему треку.") @track.command(name='vibe', description="Запустить Мою Волну по текущему треку.")
async def track_vibe(self, ctx: discord.ApplicationContext) -> None: async def track_vibe(self, ctx: discord.ApplicationContext) -> None:
logging.info(f"[VOICE] Vibe (track) command invoked by user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"[VOICE] 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):

View File

@@ -129,7 +129,7 @@ class VoiceGuildsDatabase(BaseGuildsDatabase):
tracks = self.get_tracks_list(gid, 'next') tracks = self.get_tracks_list(gid, 'next')
if not tracks: if not tracks:
return None return None
track = tracks.pop() track = tracks.pop(randint(0, len(tracks)))
self.update(gid, {'next_tracks': tracks}) self.update(gid, {'next_tracks': tracks})
return track return track

View File

@@ -5,7 +5,7 @@ from discord.ui import View, Button, Item, Select
from discord import VoiceChannel, ButtonStyle, Interaction, ApplicationContext, RawReactionActionEvent, Embed, ComponentType, SelectOption from discord import VoiceChannel, ButtonStyle, Interaction, ApplicationContext, RawReactionActionEvent, Embed, ComponentType, SelectOption
import yandex_music.exceptions import yandex_music.exceptions
from yandex_music import Track, ClientAsync from yandex_music import Track, Playlist, ClientAsync as YMClient
from MusicBot.cogs.utils.voice_extension import VoiceExtension, menu_views from MusicBot.cogs.utils.voice_extension import VoiceExtension, menu_views
class ToggleRepeatButton(Button, VoiceExtension): class ToggleRepeatButton(Button, VoiceExtension):
@@ -119,6 +119,27 @@ class LikeButton(Button, VoiceExtension):
menu_views[gid] = await MenuView(interaction).init() menu_views[gid] = await MenuView(interaction).init()
await interaction.edit(view=menu_views[gid]) await interaction.edit(view=menu_views[gid])
class DislikeButton(Button, VoiceExtension):
def __init__(self, **kwargs):
Button.__init__(self, **kwargs)
VoiceExtension.__init__(self, None)
async def callback(self, interaction: Interaction) -> None:
logging.info('[MENU] Dislike button callback...')
if not await self.voice_check(interaction):
return
if not (vc := await self.get_voice_client(interaction)) or not vc.is_playing:
await interaction.respond("❌ Нет воспроизводимого трека.", delete_after=15, ephemeral=True)
res = await self.dislike_track(interaction)
if res:
logging.debug("[VC_EXT] Disliked track")
await self.next_track(interaction, vc=vc, button_callback=True)
else:
logging.debug("[VC_EXT] Failed to dislike track")
await interaction.respond("Не удалось поставить дизлайк. Попробуйте позже.")
class LyricsButton(Button, VoiceExtension): class LyricsButton(Button, VoiceExtension):
def __init__(self, **kwargs): def __init__(self, **kwargs):
Button.__init__(self, **kwargs) Button.__init__(self, **kwargs)
@@ -137,7 +158,7 @@ class LyricsButton(Button, VoiceExtension):
track = cast(Track, Track.de_json( track = cast(Track, Track.de_json(
current_track, current_track,
ClientAsync(ym_token), # type: ignore # Async client can be used here YMClient(ym_token), # type: ignore # Async client can be used here
)) ))
try: try:
@@ -300,11 +321,85 @@ class MyVibeSettingsButton(Button, VoiceExtension):
async def callback(self, interaction: Interaction) -> None: async def callback(self, interaction: Interaction) -> None:
logging.info('[VIBE] My vibe settings button callback') logging.info('[VIBE] My vibe settings button callback')
if not await self.voice_check(interaction) or not interaction.user: if not await self.voice_check(interaction):
return return
await interaction.respond('Настройки "Моей Волны"', view=MyVibeSettingsView(interaction), ephemeral=True) await interaction.respond('Настройки "Моей Волны"', view=MyVibeSettingsView(interaction), ephemeral=True)
class AddToPlaylistSelect(Select, VoiceExtension):
def __init__(self, ym_client: YMClient, *args, **kwargs):
super().__init__(*args, **kwargs)
VoiceExtension.__init__(self, None)
self.ym_client = ym_client
async def callback(self, interaction: Interaction):
if not interaction.data or not interaction.guild_id:
return
if not interaction.data or 'values' not in interaction.data:
logging.warning('[MENU] No data in select callback')
return
data = interaction.data['values'][0].split(';')
logging.debug(f"[MENU] Add to playlist select callback: {data}")
playlist = cast(Playlist, await self.ym_client.users_playlists(kind=data[0], user_id=data[1]))
current_track = self.db.get_track(interaction.guild_id, 'current')
if not current_track:
return
try:
res = await self.ym_client.users_playlists_insert_track(
kind=f"{playlist.kind}",
track_id=current_track['id'],
album_id=current_track['albums'][0]['id'],
revision=playlist.revision or 1,
user_id=f"{playlist.uid}"
)
except yandex_music.exceptions.NetworkError:
res = None
# value=f"{playlist.kind or "-1"};{current_track['id']};{current_track['albums'][0]['id']};{playlist.revision};{playlist.uid}"
if res:
await interaction.respond('✅ Добавлено в плейлист', delete_after=15, ephemeral=True)
else:
await interaction.respond('❌ Что-то пошло не так. Попробуйте позже.', delete_after=15, ephemeral=True)
class AddToPlaylistButton(Button, VoiceExtension):
def __init__(self, **kwargs):
Button.__init__(self, **kwargs)
VoiceExtension.__init__(self, None)
async def callback(self, interaction: Interaction):
if not await self.voice_check(interaction) or not interaction.guild_id:
return
client = await self.init_ym_client(interaction)
if not client or not client.me or not client.me.account or not client.me.account.uid:
await interaction.respond('❌ Что-то пошло не так. Попробуйте позже.', ephemeral=True)
return
if not (vc := await self.get_voice_client(interaction)) or not vc.is_playing:
await interaction.respond("❌ Нет воспроизводимого трека.", delete_after=15, ephemeral=True)
return
view = View(
AddToPlaylistSelect(
client,
ComponentType.string_select,
placeholder='Выберите плейлист',
options=[
SelectOption(
label=playlist.title or "Без названия",
value=f"{playlist.kind or "-1"};{playlist.uid}"
) for playlist in await client.users_playlists_list(client.me.account.uid)
]
)
)
await interaction.respond(view=view, ephemeral=True, delete_after=360)
class MenuView(View, VoiceExtension): class MenuView(View, VoiceExtension):
def __init__(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, *items: Item, timeout: float | None = 3600, disable_on_timeout: bool = False): def __init__(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, *items: Item, timeout: float | None = 3600, disable_on_timeout: bool = False):
@@ -322,7 +417,9 @@ class MenuView(View, VoiceExtension):
self.prev_button = PrevTrackButton(style=ButtonStyle.primary, emoji='', row=0) self.prev_button = PrevTrackButton(style=ButtonStyle.primary, emoji='', row=0)
self.like_button = LikeButton(style=ButtonStyle.secondary, emoji='❤️', row=1) self.like_button = LikeButton(style=ButtonStyle.secondary, emoji='❤️', row=1)
self.dislike_button = DislikeButton(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.add_to_playlist_button = AddToPlaylistButton(style=ButtonStyle.secondary, emoji='📁', row=1)
self.vibe_button = MyVibeButton(style=ButtonStyle.secondary, emoji='🌊', row=1) self.vibe_button = MyVibeButton(style=ButtonStyle.secondary, emoji='🌊', row=1)
self.vibe_settings_button = MyVibeSettingsButton(style=ButtonStyle.success, emoji='🛠', row=1) self.vibe_settings_button = MyVibeSettingsButton(style=ButtonStyle.success, emoji='🛠', row=1)
@@ -345,7 +442,9 @@ class MenuView(View, VoiceExtension):
self.lyrics_button.disabled = True self.lyrics_button.disabled = True
self.add_item(self.like_button) self.add_item(self.like_button)
self.add_item(self.dislike_button)
self.add_item(self.lyrics_button) self.add_item(self.lyrics_button)
self.add_item(self.add_to_playlist_button)
if self.guild['vibing']: if self.guild['vibing']:
self.add_item(self.vibe_settings_button) self.add_item(self.vibe_settings_button)