feat: Add /account likes and user playlists search.

This commit is contained in:
Lemon4ksan
2025-01-17 13:56:35 +03:00
parent c2feeec158
commit b9ea63b4d5
7 changed files with 188 additions and 54 deletions

View File

@@ -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)

View File

@@ -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,

View File

@@ -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)

View File

@@ -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):

View File

@@ -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'

View File

@@ -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)

View File

@@ -2,7 +2,6 @@ import os
import logging
import discord
from discord.errors import NotFound
from discord.ext.commands import Bot
try: