mirror of
https://github.com/deadcxap/YandexMusicDiscordBot.git
synced 2026-01-10 12:21:45 +03:00
feat: Add /account likes and user playlists search.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
from math import ceil
|
||||
from typing import cast
|
||||
from asyncio import gather
|
||||
|
||||
import discord
|
||||
from discord.ext.commands import Cog
|
||||
@@ -13,7 +13,7 @@ from MusicBot.cogs.utils.find import (
|
||||
process_album, process_track, process_artist, process_playlist,
|
||||
ListenAlbum, ListenTrack, ListenArtist, ListenPlaylist
|
||||
)
|
||||
from MusicBot.cogs.utils.misc import MyPlalistsView, generate_playlist_embed
|
||||
from MusicBot.cogs.utils.misc import MyPlalists, ListenLikesPlaylist, generate_playlist_embed, generate_likes_embed
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(General(bot))
|
||||
@@ -51,9 +51,10 @@ class General(Cog):
|
||||
embed.add_field(
|
||||
name='__Основные команды__',
|
||||
value="""
|
||||
`account`
|
||||
`find`
|
||||
`help`
|
||||
`account`
|
||||
`like`
|
||||
`queue`
|
||||
`track`
|
||||
`voice`
|
||||
@@ -62,16 +63,20 @@ class General(Cog):
|
||||
|
||||
embed.set_author(name='YandexMusic')
|
||||
embed.set_footer(text='©️ Bananchiki')
|
||||
elif command == 'account':
|
||||
embed.description += ("Ввести токен от Яндекс Музыки. Его можно получить [здесь](https://github.com/MarshalX/yandex-music-api/discussions/513).\n"
|
||||
"```/account login <token>```\n"
|
||||
"Удалить токен из датабазы бота.\n```/account remove```\n"
|
||||
"Получить ваши плейлисты. Чтобы добавить плейлист в очередь, используйте команду /find.\n```/account playlists```\n"
|
||||
"Получить плейлист «Мне нравится». \n```/account likes```\n")
|
||||
elif command == 'find':
|
||||
embed.description += ("Вывести информацию о треке (по умолчанию), альбоме, авторе или плейлисте. Позволяет добавить музыку в очередь. "
|
||||
"В названии можно уточнить автора или версию. Возвращается лучшее совпадение.\n```/find <название> <тип>```")
|
||||
elif command == 'help':
|
||||
embed.description += ("Вывести список всех команд.\n```/help```\n"
|
||||
"Получить информацию о конкретной команде.\n```/help <команда>```")
|
||||
elif command == 'account':
|
||||
embed.description += ("Ввести токен от Яндекс Музыки. Его можно получить [здесь](https://github.com/MarshalX/yandex-music-api/discussions/513).\n"
|
||||
"```/account login <token>```\n"
|
||||
"Удалить токен из датабазы бота.\n```/account remove```")
|
||||
elif command == 'like':
|
||||
embed.description += "Добавить трек в плейлист «Мне нравится».\n```/like```"
|
||||
elif command == 'queue':
|
||||
embed.description += ("Получить очередь треков. По 15 элементов на страницу.\n```/queue get```\n"
|
||||
"Очистить очередь треков и историю прослушивания. Требует согласия части слушателей.\n```/queue clear```\n"
|
||||
@@ -111,7 +116,27 @@ class General(Cog):
|
||||
self.db.update(ctx.user.id, {'ym_token': None})
|
||||
await ctx.respond(f'Токен был удалён.', delete_after=15, ephemeral=True)
|
||||
|
||||
@account.command(description="Получить плейлисты пользователя.")
|
||||
@account.command(description="Получить плейлист «Мне нравится»")
|
||||
async def likes(self, ctx: discord.ApplicationContext) -> None:
|
||||
token = self.db.get_ym_token(ctx.user.id)
|
||||
if not token:
|
||||
await ctx.respond('❌ Необходимо указать свой токен доступа с помощью команды /login.', delete_after=15, ephemeral=True)
|
||||
return
|
||||
client = await YMClient(token).init()
|
||||
if not client.me or not client.me.account or not client.me.account.uid:
|
||||
await ctx.respond('❌ Что-то пошло не так. Повторите попытку позже.', delete_after=15, ephemeral=True)
|
||||
return
|
||||
likes = await client.users_likes_tracks()
|
||||
if not likes:
|
||||
await ctx.respond('❌ Что-то пошло не так. Повторите попытку позже.', delete_after=15, ephemeral=True)
|
||||
return
|
||||
|
||||
real_tracks = await gather(*[track_short.fetch_track_async() for track_short in likes.tracks], return_exceptions=True)
|
||||
tracks = [track for track in real_tracks if not isinstance(track, BaseException)] # Can't fetch user tracks
|
||||
embed = generate_likes_embed(tracks)
|
||||
await ctx.respond(embed=embed, view=ListenLikesPlaylist(tracks))
|
||||
|
||||
@account.command(description="Получить ваши плейлисты.")
|
||||
async def playlists(self, ctx: discord.ApplicationContext) -> None:
|
||||
token = self.db.get_ym_token(ctx.user.id)
|
||||
if not token:
|
||||
@@ -125,7 +150,7 @@ class General(Cog):
|
||||
playlists: list[tuple[str, int]] = [(playlist.title, playlist.track_count) for playlist in playlists_list] # type: ignore
|
||||
self.db.update(ctx.user.id, {'playlists': playlists, 'playlists_page': 0})
|
||||
embed = generate_playlist_embed(0, playlists)
|
||||
await ctx.respond(embed=embed, view=MyPlalistsView(ctx), ephemeral=True)
|
||||
await ctx.respond(embed=embed, view=MyPlalists(ctx), ephemeral=True)
|
||||
|
||||
@discord.slash_command(description="Найти контент и отправить информацию о нём. Возвращается лучшее совпадение.")
|
||||
@discord.option(
|
||||
@@ -137,7 +162,7 @@ class General(Cog):
|
||||
"content_type",
|
||||
description="Тип искомого контента.",
|
||||
type=discord.SlashCommandOptionType.string,
|
||||
choices=['Artist', 'Album', 'Track', 'Playlist'],
|
||||
choices=['Artist', 'Album', 'Track', 'Playlist', 'User Playlist'],
|
||||
default='Track'
|
||||
)
|
||||
async def find(
|
||||
@@ -146,10 +171,9 @@ class General(Cog):
|
||||
name: str,
|
||||
content_type: str = 'Track'
|
||||
) -> None:
|
||||
if content_type not in ['Artist', 'Album', 'Track', 'Playlist']:
|
||||
if content_type not in ['Artist', 'Album', 'Track', 'Playlist', 'User Playlist']:
|
||||
await ctx.respond("❌ Недопустимый тип.", delete_after=15, ephemeral=True)
|
||||
return
|
||||
content_type = content_type.lower()
|
||||
|
||||
token = self.db.get_ym_token(ctx.user.id)
|
||||
if not token:
|
||||
@@ -161,28 +185,45 @@ class General(Cog):
|
||||
await ctx.respond("❌ Недействительный токен. Если это не так, попробуйте ещё раз.", delete_after=15, ephemeral=True)
|
||||
return
|
||||
|
||||
result = await client.search(name, True, content_type)
|
||||
|
||||
if not result:
|
||||
await ctx.respond("❌ Что-то пошло не так. Повторите попытку позже", delete_after=15, ephemeral=True)
|
||||
return
|
||||
|
||||
if content_type == 'album' and result.albums:
|
||||
album = result.albums.results[0]
|
||||
embed = await process_album(album)
|
||||
await ctx.respond(embed=embed, view=ListenAlbum(album))
|
||||
elif content_type == 'track' and result.tracks:
|
||||
track: yandex_music.Track = result.tracks.results[0]
|
||||
album_id = cast(int, track.albums[0].id)
|
||||
embed = await process_track(track)
|
||||
await ctx.respond(embed=embed, view=ListenTrack(track, album_id))
|
||||
elif content_type == 'artist' and result.artists:
|
||||
artist = result.artists.results[0]
|
||||
embed = await process_artist(artist)
|
||||
await ctx.respond(embed=embed, view=ListenArtist(artist))
|
||||
elif content_type == 'playlist' and result.playlists:
|
||||
playlist = result.playlists.results[0]
|
||||
embed = await process_playlist(playlist)
|
||||
await ctx.respond(embed=embed, view=ListenPlaylist(playlist))
|
||||
if content_type == 'User Playlist':
|
||||
if not client.me or not client.me.account or not client.me.account.uid:
|
||||
await ctx.respond("❌ Не удалось получить информацию о пользователе.", delete_after=15, ephemeral=True)
|
||||
return
|
||||
playlists = await client.users_playlists_list(client.me.account.uid)
|
||||
result = None
|
||||
for playlist in playlists:
|
||||
if playlist.title == name:
|
||||
result = playlist
|
||||
break
|
||||
else:
|
||||
await ctx.respond("❌ Плейлист не найден.", delete_after=15, ephemeral=True)
|
||||
return
|
||||
|
||||
embed = await process_playlist(result)
|
||||
await ctx.respond(embed=embed, view=ListenPlaylist(result))
|
||||
else:
|
||||
await ctx.respond("❌ По запросу ничего не найдено.", delete_after=15, ephemeral=True)
|
||||
result = await client.search(name, True, content_type.lower())
|
||||
|
||||
if not result:
|
||||
await ctx.respond("❌ Что-то пошло не так. Повторите попытку позже", delete_after=15, ephemeral=True)
|
||||
return
|
||||
|
||||
if content_type == 'Album' and result.albums:
|
||||
album = result.albums.results[0]
|
||||
embed = await process_album(album)
|
||||
await ctx.respond(embed=embed, view=ListenAlbum(album))
|
||||
elif content_type == 'Track' and result.tracks:
|
||||
track: yandex_music.Track = result.tracks.results[0]
|
||||
album_id = cast(int, track.albums[0].id)
|
||||
embed = await process_track(track)
|
||||
await ctx.respond(embed=embed, view=ListenTrack(track, album_id))
|
||||
elif content_type == 'Artist' and result.artists:
|
||||
artist = result.artists.results[0]
|
||||
embed = await process_artist(artist)
|
||||
await ctx.respond(embed=embed, view=ListenArtist(artist))
|
||||
elif content_type == 'Playlist' and result.playlists:
|
||||
playlist = result.playlists.results[0]
|
||||
embed = await process_playlist(playlist)
|
||||
await ctx.respond(embed=embed, view=ListenPlaylist(playlist))
|
||||
else:
|
||||
await ctx.respond("❌ По запросу ничего не найдено.", delete_after=15, ephemeral=True)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from asyncio import gather
|
||||
from os import getenv
|
||||
from math import ceil
|
||||
from typing import cast
|
||||
@@ -8,7 +9,7 @@ from yandex_music import Track, Album, Artist, Playlist, Label
|
||||
from discord.ui import View, Button, Item
|
||||
from discord import ButtonStyle, Interaction, Embed
|
||||
|
||||
from MusicBot.cogs.utils.voice import VoiceExtension, get_average_color_from_url
|
||||
from MusicBot.cogs.utils.voice_extension import VoiceExtension, get_average_color_from_url
|
||||
|
||||
class PlayTrackButton(Button, VoiceExtension):
|
||||
|
||||
@@ -120,7 +121,8 @@ class PlayPlaylistButton(Button, VoiceExtension):
|
||||
gid = interaction.guild.id
|
||||
guild = self.db.get_guild(gid)
|
||||
|
||||
tracks: list[Track] = [cast(Track, short_track.track) for short_track in short_tracks]
|
||||
real_tracks = await gather(*[track_short.fetch_track_async() for track_short in self.playlist.tracks], return_exceptions=True)
|
||||
tracks = [track for track in real_tracks if not isinstance(track, BaseException)] # Can't fetch user tracks
|
||||
|
||||
if guild['current_track'] is not None:
|
||||
self.db.modify_track(gid, tracks, 'next', 'extend')
|
||||
@@ -418,8 +420,23 @@ async def process_playlist(playlist: Playlist) -> Embed:
|
||||
duration = playlist.duration_ms
|
||||
likes_count = playlist.likes_count
|
||||
|
||||
cover_url = f"https://{playlist.cover.uri.replace('%%', '400x400')}" # type: ignore
|
||||
color = await get_average_color_from_url(cover_url)
|
||||
color = 0x000
|
||||
cover_url = None
|
||||
|
||||
if playlist.cover and playlist.cover.uri:
|
||||
cover_url = f"https://{playlist.cover.uri.replace('%%', '400x400')}"
|
||||
else:
|
||||
tracks = await playlist.fetch_tracks_async()
|
||||
for i in range(len(tracks)):
|
||||
track = tracks[i].track
|
||||
try:
|
||||
cover_url = f"https://{track.albums[0].cover_uri.replace('%%', '400x400')}" # type: ignore
|
||||
break
|
||||
except TypeError:
|
||||
continue
|
||||
|
||||
if cover_url:
|
||||
color = await get_average_color_from_url(cover_url)
|
||||
|
||||
embed = discord.Embed(
|
||||
title=title,
|
||||
|
||||
@@ -1,10 +1,36 @@
|
||||
from math import ceil
|
||||
from typing import Any
|
||||
|
||||
from yandex_music import Track
|
||||
from discord.ui import View, Button, Item
|
||||
from discord import ButtonStyle, Interaction, ApplicationContext, Embed
|
||||
|
||||
from MusicBot.cogs.utils.voice import VoiceExtension
|
||||
from MusicBot.cogs.utils.voice_extension import VoiceExtension
|
||||
|
||||
def generate_likes_embed(tracks: list[Track]) -> Embed:
|
||||
track_count = len(tracks)
|
||||
cover_url = "https://avatars.yandex.net/get-music-user-playlist/11418140/favorit-playlist-cover.bb48fdb9b9f4/300x300"
|
||||
|
||||
embed = Embed(
|
||||
title="Мне нравится",
|
||||
description="Треки, которые вам понравились.",
|
||||
color=0xce3a26,
|
||||
)
|
||||
embed.set_thumbnail(url=cover_url)
|
||||
|
||||
duration = 0
|
||||
for track in tracks:
|
||||
if track.duration_ms:
|
||||
duration += track.duration_ms
|
||||
|
||||
duration_m = duration // 60000
|
||||
duration_s = ceil(duration / 1000) - duration_m * 60
|
||||
embed.add_field(name="Длительность", value=f"{duration_m}:{duration_s:02}")
|
||||
|
||||
if track_count is not None:
|
||||
embed.add_field(name="Треки", value=str(track_count))
|
||||
|
||||
return embed
|
||||
|
||||
def generate_playlist_embed(page: int, playlists: list[tuple[str, int]]) -> Embed:
|
||||
count = 15 * page
|
||||
@@ -35,6 +61,38 @@ def generate_queue_embed(page: int, tracks_list: list[dict[str, Any]]) -> Embed:
|
||||
embed.add_field(name=f"{i} - {track['title']} - {duration_m}:{duration_s:02d}", value="", inline=False)
|
||||
return embed
|
||||
|
||||
class PlayLikesButton(Button, VoiceExtension):
|
||||
def __init__(self, playlist: list[Track], **kwargs):
|
||||
Button.__init__(self, **kwargs)
|
||||
VoiceExtension.__init__(self)
|
||||
self.playlist = playlist
|
||||
|
||||
async def callback(self, interaction: Interaction):
|
||||
if not interaction.guild or not await self.voice_check(interaction):
|
||||
return
|
||||
|
||||
gid = interaction.guild.id
|
||||
guild = self.db.get_guild(gid)
|
||||
|
||||
if guild['current_track'] is not None:
|
||||
self.db.modify_track(gid, self.playlist, 'next', 'extend')
|
||||
response_message = f"Плейлист **«Мне нравится»** был добавлен в очередь."
|
||||
else:
|
||||
track = self.playlist.pop(0)
|
||||
self.db.modify_track(gid, self.playlist, 'next', 'extend')
|
||||
await self.play_track(interaction, track)
|
||||
response_message = f"Сейчас играет плейлист **«Мне нравится»**!"
|
||||
|
||||
if guild['current_player'] is not None and interaction.message:
|
||||
await interaction.message.delete()
|
||||
|
||||
await interaction.respond(response_message, delete_after=15)
|
||||
|
||||
class ListenLikesPlaylist(View):
|
||||
def __init__(self, playlist: list[Track], *items: Item, timeout: float | None = None, disable_on_timeout: bool = False):
|
||||
super().__init__(*items, timeout=timeout, disable_on_timeout=disable_on_timeout)
|
||||
self.add_item(PlayLikesButton(playlist, label="Слушать в голосовом канале", style=ButtonStyle.gray))
|
||||
|
||||
class MPNextButton(Button, VoiceExtension):
|
||||
def __init__(self, **kwargs):
|
||||
Button.__init__(self, **kwargs)
|
||||
@@ -47,7 +105,7 @@ class MPNextButton(Button, VoiceExtension):
|
||||
page = user['playlists_page'] + 1
|
||||
self.users_db.update(interaction.user.id, {'playlists_page': page})
|
||||
embed = generate_playlist_embed(page, user['playlists'])
|
||||
await interaction.edit(embed=embed, view=MyPlalistsView(interaction))
|
||||
await interaction.edit(embed=embed, view=MyPlalists(interaction))
|
||||
|
||||
class MPPrevButton(Button, VoiceExtension):
|
||||
def __init__(self, **kwargs):
|
||||
@@ -61,9 +119,9 @@ class MPPrevButton(Button, VoiceExtension):
|
||||
page = user['playlists_page'] - 1
|
||||
self.users_db.update(interaction.user.id, {'playlists_page': page})
|
||||
embed = generate_playlist_embed(page, user['playlists'])
|
||||
await interaction.edit(embed=embed, view=MyPlalistsView(interaction))
|
||||
await interaction.edit(embed=embed, view=MyPlalists(interaction))
|
||||
|
||||
class MyPlalistsView(View, VoiceExtension):
|
||||
class MyPlalists(View, VoiceExtension):
|
||||
def __init__(self, ctx: ApplicationContext | Interaction, *items: Item, timeout: float | None = 3600, disable_on_timeout: bool = True):
|
||||
View.__init__(self, *items, timeout=timeout, disable_on_timeout=disable_on_timeout)
|
||||
VoiceExtension.__init__(self)
|
||||
@@ -83,7 +141,6 @@ class MyPlalistsView(View, VoiceExtension):
|
||||
self.add_item(prev_button)
|
||||
self.add_item(next_button)
|
||||
|
||||
|
||||
class QNextButton(Button, VoiceExtension):
|
||||
def __init__(self, **kwargs):
|
||||
Button.__init__(self, **kwargs)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from discord.ui import View, Button, Item
|
||||
from discord import ButtonStyle, Interaction, ApplicationContext
|
||||
|
||||
from MusicBot.cogs.utils.voice import VoiceExtension
|
||||
from MusicBot.cogs.utils.voice_extension import VoiceExtension
|
||||
|
||||
class ToggleRepeatButton(Button, VoiceExtension):
|
||||
def __init__(self, **kwargs):
|
||||
@@ -77,6 +77,24 @@ class PrevTrackButton(Button, VoiceExtension):
|
||||
if not title:
|
||||
await interaction.respond(f"Нет треков в истории.", delete_after=15, ephemeral=True)
|
||||
|
||||
class LikeButton(Button, VoiceExtension):
|
||||
def __init__(self, **kwargs):
|
||||
Button.__init__(self, **kwargs)
|
||||
VoiceExtension.__init__(self)
|
||||
|
||||
async def callback(self, interaction: Interaction) -> None:
|
||||
if await self.voice_check(interaction):
|
||||
vc = self.get_voice_client(interaction)
|
||||
if not vc or not vc.is_playing:
|
||||
await interaction.respond("Нет воспроизводимого трека.", delete_after=15, ephemeral=True)
|
||||
result = await self.like_track(interaction)
|
||||
if not result:
|
||||
await interaction.respond("❌ Операция не удалась.", delete_after=15, ephemeral=True)
|
||||
elif result == 'TRACK REMOVED':
|
||||
await interaction.respond("Трек был удалён из избранного.", delete_after=15, ephemeral=True)
|
||||
else:
|
||||
await interaction.respond(f"Трек **{result}** был добавлен в избранное.", delete_after=15, ephemeral=True)
|
||||
|
||||
class Player(View, VoiceExtension):
|
||||
|
||||
def __init__(self, ctx: ApplicationContext | Interaction, *items: Item, timeout: float | None = 3600, disable_on_timeout: bool = True):
|
||||
|
||||
@@ -2,7 +2,7 @@ import aiohttp
|
||||
import asyncio
|
||||
from os import getenv
|
||||
from math import ceil
|
||||
from typing import cast
|
||||
from typing import Literal, cast
|
||||
from io import BytesIO
|
||||
from PIL import Image
|
||||
|
||||
@@ -359,7 +359,7 @@ class VoiceExtension:
|
||||
|
||||
return None
|
||||
|
||||
async def like_track(self, ctx: ApplicationContext | Interaction) -> str | None:
|
||||
async def like_track(self, ctx: ApplicationContext | Interaction) -> str | Literal['TRACK REMOVED'] | None:
|
||||
"""Like current track. Return track title on success.
|
||||
|
||||
Args:
|
||||
@@ -385,6 +385,7 @@ class VoiceExtension:
|
||||
if ym_track.id not in [track.id for track in likes.tracks]:
|
||||
await ym_track.like_async()
|
||||
return ym_track.title
|
||||
|
||||
return None
|
||||
else:
|
||||
await client.users_likes_tracks_remove(ym_track.id, client.me.account.uid) # type: ignore
|
||||
return 'TRACK REMOVED'
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from math import ceil
|
||||
from typing import cast
|
||||
|
||||
import discord
|
||||
@@ -6,7 +5,7 @@ from discord.ext.commands import Cog
|
||||
|
||||
from yandex_music import Track, ClientAsync
|
||||
|
||||
from MusicBot.cogs.utils.voice import VoiceExtension, generate_player_embed
|
||||
from MusicBot.cogs.utils.voice_extension import VoiceExtension, generate_player_embed
|
||||
from MusicBot.cogs.utils.player import Player
|
||||
from MusicBot.cogs.utils.misc import QueueView, generate_queue_embed
|
||||
|
||||
@@ -136,6 +135,8 @@ class Voice(Cog, VoiceExtension):
|
||||
await ctx.respond("Нет воспроизводимого трека.", delete_after=15, ephemeral=True)
|
||||
result = await self.like_track(ctx)
|
||||
if not result:
|
||||
await ctx.respond("Трек уже добавлен в избранное.", delete_after=15, ephemeral=True)
|
||||
await ctx.respond("❌ Операция не удалась.", delete_after=15, ephemeral=True)
|
||||
elif result == 'TRACK REMOVED':
|
||||
await ctx.respond("Трек был удалён из избранного.", delete_after=15, ephemeral=True)
|
||||
else:
|
||||
await ctx.respond(f"Трек **{result}** был добавлен в избранное.", delete_after=15, ephemeral=True)
|
||||
|
||||
Reference in New Issue
Block a user