impr: Update bot to work with updated database and use tracks history.

This commit is contained in:
Lemon4ksan
2025-01-09 12:55:59 +03:00
parent 7d462442cc
commit d797fec45f
5 changed files with 130 additions and 64 deletions

View File

@@ -7,7 +7,7 @@ import yandex_music
import yandex_music.exceptions import yandex_music.exceptions
from yandex_music import ClientAsync as YMClient from yandex_music import ClientAsync as YMClient
from MusicBot.database.base import get_ym_token, update from MusicBot.database import BaseUsersDatabase
from MusicBot.cogs.utils.find import ( from MusicBot.cogs.utils.find import (
proccess_album, process_track, process_artist, proccess_album, process_track, process_artist,
ListenAlbum, ListenTrack, ListenArtist ListenAlbum, ListenTrack, ListenArtist
@@ -20,6 +20,7 @@ class General(Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
self.db = BaseUsersDatabase()
@discord.slash_command(description="Login to Yandex Music using access token.", guild_ids=[1247100229535141899]) @discord.slash_command(description="Login to Yandex Music using access token.", guild_ids=[1247100229535141899])
@discord.option("token", type=discord.SlashCommandOptionType.string) @discord.option("token", type=discord.SlashCommandOptionType.string)
@@ -32,8 +33,8 @@ class General(Cog):
about = cast(yandex_music.Status, client.me).to_dict() about = cast(yandex_music.Status, client.me).to_dict()
uid = ctx.author.id uid = ctx.author.id
update(uid, {'ym_token': token}) self.db.update(uid, {'ym_token': token})
await ctx.respond(f'Привет, {about['account']['first_name']}!', ephemeral=True) await ctx.respond(f'Привет, {about['account']['first_name']}!', delete_after=15, ephemeral=True)
@discord.slash_command(description="Find the content type by its name and send info about it. The best match is returned.", guild_ids=[1247100229535141899]) @discord.slash_command(description="Find the content type by its name and send info about it. The best match is returned.", guild_ids=[1247100229535141899])
@discord.option( @discord.option(
@@ -52,8 +53,7 @@ class General(Cog):
await ctx.respond('❌ Недопустимый тип.') await ctx.respond('❌ Недопустимый тип.')
return return
token = get_ym_token(ctx.user.id) token = self.db.get_ym_token(ctx.user.id)
if not token: if not token:
await ctx.respond('❌ Необходимо указать свой токен доступа с помощью комманды /login.', delete_after=15, ephemeral=True) await ctx.respond('❌ Необходимо указать свой токен доступа с помощью комманды /login.', delete_after=15, ephemeral=True)
return return
@@ -68,13 +68,13 @@ class General(Cog):
if content_type == 'album': if content_type == 'album':
album = result.albums.results[0] # type: ignore album = result.albums.results[0] # type: ignore
embed = await proccess_album(album) embed = await proccess_album(album)
await ctx.respond("", embed=embed, view=ListenAlbum(album)) await ctx.respond("", embed=embed, view=ListenAlbum(album), delete_after=360)
elif content_type == 'track': elif content_type == 'track':
track: yandex_music.Track = result.tracks.results[0] # type: ignore track: yandex_music.Track = result.tracks.results[0] # type: ignore
album_id = cast(int, track.albums[0].id) album_id = cast(int, track.albums[0].id)
embed = await process_track(track) embed = await process_track(track)
await ctx.respond("", embed=embed, view=ListenTrack(track, album_id)) await ctx.respond("", embed=embed, view=ListenTrack(track, album_id), delete_after=360)
elif content_type == 'artist': elif content_type == 'artist':
artist = result.artists.results[0] # type: ignore artist = result.artists.results[0] # type: ignore
embed = await process_artist(artist) embed = await process_artist(artist)
await ctx.respond("", embed=embed, view=ListenArtist(artist.id)) await ctx.respond("", embed=embed, view=ListenArtist(artist.id), delete_after=360)

View File

@@ -11,7 +11,7 @@ from discord.ui import View, Button, Item
from discord import ButtonStyle, Interaction from discord import ButtonStyle, Interaction
from MusicBot.cogs.utils.voice import VoiceExtension from MusicBot.cogs.utils.voice import VoiceExtension
from MusicBot.database.base import add_track, pop_track from MusicBot.database import VoiceGuildsDatabase, BaseUsersDatabase
class PlayTrackButton(Button, VoiceExtension): class PlayTrackButton(Button, VoiceExtension):
@@ -58,9 +58,9 @@ class PlayAlbumButton(Button, VoiceExtension):
Button.__init__(self, style=style, label=label, disabled=disabled, custom_id=custom_id, url=url, emoji=emoji, sku_id=sku_id, row=row) Button.__init__(self, style=style, label=label, disabled=disabled, custom_id=custom_id, url=url, emoji=emoji, sku_id=sku_id, row=row)
VoiceExtension.__init__(self) VoiceExtension.__init__(self)
self.album = album self.album = album
async def callback(self, interaction: Interaction) -> None: async def callback(self, interaction: Interaction) -> None:
if not interaction.user: if not interaction.guild:
return return
album = cast(yandex_music.Album, await self.album.with_tracks_async()) album = cast(yandex_music.Album, await self.album.with_tracks_async())
@@ -69,11 +69,11 @@ class PlayAlbumButton(Button, VoiceExtension):
for volume in album.volumes: for volume in album.volumes:
for track in volume: for track in volume:
add_track(interaction.user.id, track) self.db.add_track(interaction.guild.id, track)
track = pop_track(interaction.user.id) track = self.db.pop_track(interaction.guild.id)
ym_track = yandex_music.Track(id=track['track_id'], title=track['title'], client=album.client) # type: ignore ym_track = yandex_music.Track.de_json(track, client=album.client) # type: ignore
title = await self.play_track(interaction, ym_track) title = await self.play_track(interaction, ym_track) # type: ignore
if title: if title:
await interaction.respond(f"Сейчас играет: **{album.title}**!", delete_after=15) await interaction.respond(f"Сейчас играет: **{album.title}**!", delete_after=15)
else: else:
@@ -81,7 +81,7 @@ class PlayAlbumButton(Button, VoiceExtension):
class ListenTrack(View): class ListenTrack(View):
def __init__(self, track: yandex_music.Track, album_id: int, *items: Item, timeout: float | None = 3600, disable_on_timeout: bool = False): def __init__(self, track: yandex_music.Track, album_id: int, *items: Item, timeout: float | None = 360, disable_on_timeout: bool = False):
super().__init__(*items, timeout=timeout, disable_on_timeout=disable_on_timeout) super().__init__(*items, timeout=timeout, disable_on_timeout=disable_on_timeout)
link_app = f"yandexmusic://album/{album_id}/track/{track.id}" link_app = f"yandexmusic://album/{album_id}/track/{track.id}"
link_web = f"https://music.yandex.ru/album/{album_id}/track/{track.id}" link_web = f"https://music.yandex.ru/album/{album_id}/track/{track.id}"
@@ -94,7 +94,7 @@ class ListenTrack(View):
class ListenAlbum(View): class ListenAlbum(View):
def __init__(self, album: yandex_music.Album, *items: Item, timeout: float | None = 180, disable_on_timeout: bool = False): def __init__(self, album: yandex_music.Album, *items: Item, timeout: float | None = 360, disable_on_timeout: bool = False):
super().__init__(*items, timeout=timeout, disable_on_timeout=disable_on_timeout) super().__init__(*items, timeout=timeout, disable_on_timeout=disable_on_timeout)
link_app = f"yandexmusic://album/{album.id}" link_app = f"yandexmusic://album/{album.id}"
link_web = f"https://music.yandex.ru/album/{album.id}" link_web = f"https://music.yandex.ru/album/{album.id}"
@@ -107,7 +107,7 @@ class ListenAlbum(View):
class ListenArtist(View): class ListenArtist(View):
def __init__(self, artist_id, *items: Item, timeout: float | None = 180, disable_on_timeout: bool = False): def __init__(self, artist_id, *items: Item, timeout: float | None = 360, disable_on_timeout: bool = False):
super().__init__(*items, timeout=timeout, disable_on_timeout=disable_on_timeout) super().__init__(*items, timeout=timeout, disable_on_timeout=disable_on_timeout)
link_app = f"yandexmusic://artist/{artist_id}" link_app = f"yandexmusic://artist/{artist_id}"
link_web = f"https://music.yandex.ru/artist/{artist_id}" link_web = f"https://music.yandex.ru/artist/{artist_id}"

View File

@@ -1,11 +1,13 @@
from typing import cast
from discord.ui import View, Button, Item from discord.ui import View, Button, Item
from discord import ButtonStyle, Interaction, ApplicationContext from discord import ButtonStyle, Interaction, ApplicationContext
from MusicBot.cogs.utils.voice import VoiceExtension from MusicBot.cogs.utils.voice import VoiceExtension
class PlayPauseButton(Button, VoiceExtension): class PlayPauseButton(Button, VoiceExtension):
def __init__(self, **kwargs):
Button.__init__(self, **kwargs)
VoiceExtension.__init__(self)
async def callback(self, interaction: Interaction) -> None: async def callback(self, interaction: Interaction) -> None:
vc = self.get_voice_client(interaction) vc = self.get_voice_client(interaction)
if vc is not None: if vc is not None:
@@ -16,11 +18,24 @@ class PlayPauseButton(Button, VoiceExtension):
self.resume_playing(interaction) self.resume_playing(interaction)
await interaction.edit(content="Результат возобновления.") await interaction.edit(content="Результат возобновления.")
class NextTrackButton(Button, VoiceExtension): class NextTrackButton(Button, VoiceExtension):
def __init__(self, **kwargs):
Button.__init__(self, **kwargs)
VoiceExtension.__init__(self)
async def callback(self, interaction: Interaction) -> None: async def callback(self, interaction: Interaction) -> None:
await self.next_track(interaction) await self.next_track(interaction)
await interaction.edit(content='Результат переключения >.') await interaction.edit(content='Результат переключения >.')
class PrevTrackButton(Button, VoiceExtension):
def __init__(self, **kwargs):
Button.__init__(self, **kwargs)
VoiceExtension.__init__(self)
async def callback(self, interaction: Interaction) -> None:
await self.prev_track(interaction)
await interaction.edit(content='Результат переключения <.')
class Player(View): class Player(View):
def __init__(self, ctx: ApplicationContext, *items: Item, timeout: float | None = 3600, disable_on_timeout: bool = False): def __init__(self, ctx: ApplicationContext, *items: Item, timeout: float | None = 3600, disable_on_timeout: bool = False):
@@ -33,7 +48,7 @@ class Player(View):
self.queue_button = Button(style=ButtonStyle.primary, emoji='📋', row=0) self.queue_button = Button(style=ButtonStyle.primary, emoji='📋', row=0)
self.play_pause_button = PlayPauseButton(style=ButtonStyle.primary, emoji='', row=0) self.play_pause_button = PlayPauseButton(style=ButtonStyle.primary, emoji='', row=0)
self.next_button = NextTrackButton(style=ButtonStyle.primary, emoji='', row=0) self.next_button = NextTrackButton(style=ButtonStyle.primary, emoji='', row=0)
self.prev_button = Button(style=ButtonStyle.primary, emoji='', row=0) self.prev_button = PrevTrackButton(style=ButtonStyle.primary, emoji='', row=0)
self.add_item(self.repeat_button) self.add_item(self.repeat_button)
self.add_item(self.prev_button) self.add_item(self.prev_button)

View File

@@ -7,13 +7,17 @@ from yandex_music import Track, ClientAsync
import discord import discord
from discord import Interaction, ApplicationContext from discord import Interaction, ApplicationContext
from MusicBot.database.base import update, get_user, pop_track, add_track, set_current_track from MusicBot.database import VoiceGuildsDatabase, BaseUsersDatabase
class VoiceExtension: class VoiceExtension:
def __init__(self) -> None:
self.db = VoiceGuildsDatabase()
self.users_db = BaseUsersDatabase()
def clear_queue(self, ctx: ApplicationContext | Interaction): def clear_queue(self, ctx: ApplicationContext | Interaction):
if ctx.user: if ctx.guild:
update(ctx.user.id, {'tracks_list': []}) self.db.update(ctx.guild.id, {'tracks_list': []})
def get_voice_client(self, ctx: ApplicationContext | Interaction) -> discord.VoiceClient | None: def get_voice_client(self, ctx: ApplicationContext | Interaction) -> discord.VoiceClient | None:
"""Return voice client for the given guild id. Return None if not present. """Return voice client for the given guild id. Return None if not present.
@@ -30,8 +34,8 @@ class VoiceExtension:
else: else:
voice_chat = discord.utils.get(ctx.bot.voice_clients, guild=ctx.guild) voice_chat = discord.utils.get(ctx.bot.voice_clients, guild=ctx.guild)
return cast(discord.VoiceClient, voice_chat) return cast(discord.VoiceClient, voice_chat)
async def play_track(self, ctx: ApplicationContext | Interaction, track: Track) -> str | None: async def play_track(self, ctx: ApplicationContext | Interaction, track: Track) -> str | None:
"""Download ``track`` by its id and play it in the voice channel. Return track title on success and don't respond. """Download ``track`` by its id and play it in the voice channel. Return track title on success and don't respond.
If sound is already playing, add track id to the queue and respond. If sound is already playing, add track id to the queue and respond.
@@ -43,12 +47,11 @@ class VoiceExtension:
Returns: Returns:
str | None: Song title or None. str | None: Song title or None.
""" """
if not ctx.user: if not ctx.guild:
return return
vc = self.get_voice_client(ctx) vc = self.get_voice_client(ctx)
if not vc: if not vc:
await ctx.respond("Добавьте бота в голосовой канал при помощи команды /voice join.", delete_after=15, ephemeral=True)
return return
if isinstance(ctx, Interaction): if isinstance(ctx, Interaction):
@@ -56,10 +59,10 @@ class VoiceExtension:
else: else:
loop = ctx.bot.loop loop = ctx.bot.loop
uid = ctx.user.id gid = ctx.guild.id
user = get_user(uid) guild = self.db.get_guild(gid)
if user.get('current_track') is not None: if guild.get('current_track') is not None:
add_track(uid, track) self.db.add_track(gid, track)
await ctx.respond(f"Трек **{track.title}** был добавлен в очередь.", delete_after=15) await ctx.respond(f"Трек **{track.title}** был добавлен в очередь.", delete_after=15)
else: else:
await track.download_async(f'music/{ctx.guild_id}.mp3') await track.download_async(f'music/{ctx.guild_id}.mp3')
@@ -67,33 +70,27 @@ class VoiceExtension:
vc.play(song, after=lambda exc: asyncio.run_coroutine_threadsafe(self.next_track(ctx), loop)) vc.play(song, after=lambda exc: asyncio.run_coroutine_threadsafe(self.next_track(ctx), loop))
set_current_track(uid) self.db.set_current_track(gid, track)
update(uid, {'is_stopped': False}) self.db.update(gid, {'is_stopped': False})
return track.title return track.title
def pause_playing(self, ctx: ApplicationContext | Interaction) -> None: def pause_playing(self, ctx: ApplicationContext | Interaction) -> None:
if not ctx.user:
return
vc = self.get_voice_client(ctx) vc = self.get_voice_client(ctx)
if vc: if vc:
vc.pause() vc.pause()
def resume_playing(self, ctx: ApplicationContext | Interaction) -> None: def resume_playing(self, ctx: ApplicationContext | Interaction) -> None:
if not ctx.user:
return
vc = self.get_voice_client(ctx) vc = self.get_voice_client(ctx)
if vc: if vc:
vc.resume() vc.resume()
def stop_playing(self, ctx: ApplicationContext | Interaction) -> None: def stop_playing(self, ctx: ApplicationContext | Interaction) -> None:
if not ctx.user: if not ctx.guild:
return return
vc = self.get_voice_client(ctx) vc = self.get_voice_client(ctx)
if vc: if vc:
update(ctx.user.id, {'current_track': None, 'is_stopped': True}) self.db.update(ctx.guild.id, {'current_track': None, 'is_stopped': True})
vc.stop() vc.stop()
async def next_track(self, ctx: ApplicationContext | Interaction) -> str | None: async def next_track(self, ctx: ApplicationContext | Interaction) -> str | None:
@@ -106,22 +103,77 @@ class VoiceExtension:
Returns: Returns:
str | None: Track title or None. str | None: Track title or None.
""" """
if not ctx.user: if not ctx.guild or not ctx.user:
return return
uid = ctx.user.id gid = ctx.guild.id
user = get_user(uid) guild = self.db.get_guild(gid)
if user.get('is_stopped'): token = self.users_db.get_ym_token(ctx.user.id)
if guild.get('is_stopped'):
return return
if not self.get_voice_client(ctx): # Silently return if bot got kicked if not self.get_voice_client(ctx): # Silently return if bot got kicked
return return
tracks_list = user.get('tracks_list') current_track = guild.get('current_track')
if tracks_list: tracks_list = guild.get('tracks_list')
track = pop_track(uid) if tracks_list and current_track:
ym_track = Track(id=track['track_id'], title=track['title'], client=ClientAsync(user.get('ym_token'))) # type: ignore self.db.add_previous_track(gid, current_track)
track = self.db.pop_track(gid)
ym_track = Track.de_json(track, client=ClientAsync(token)) # type: ignore
self.stop_playing(ctx) self.stop_playing(ctx)
return await self.play_track(ctx, ym_track) return await self.play_track(ctx, ym_track) # type: ignore
else: elif current_track:
self.stop_playing(ctx) self.stop_playing(ctx)
async def prev_track(self, ctx: ApplicationContext | Interaction) -> str | None:
"""Switch to the previous track in the queue. Repeat curren the song if no previous tracks.
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:
return
gid = ctx.guild.id
guild = self.db.get_guild(gid)
token = self.users_db.get_ym_token(ctx.user.id)
current_track = guild.get('current_track')
tracks_list = self.db.get_previous_tracks_list(gid)
if tracks_list and current_track:
self.db.insert_track(gid, current_track)
track = self.db.pop_previous_track(gid)
ym_track = Track.de_json(track, client=ClientAsync(token)) # type: ignore
self.stop_playing(ctx)
return await self.play_track(ctx, ym_track) # type: ignore
elif current_track:
return await self.repeat_current_track(ctx)
async def repeat_current_track(self, ctx: ApplicationContext | Interaction) -> str | None:
"""Repeat 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:
return
gid = ctx.guild.id
guild = self.db.get_guild(gid)
token = self.users_db.get_ym_token(ctx.user.id)
current_track = guild.get('current_track')
if current_track:
ym_track = Track.de_json(current_track, client=ClientAsync(token)) # type: ignore
self.stop_playing(ctx)
return await self.play_track(ctx, ym_track) # type: ignore

View File

@@ -4,8 +4,6 @@ from discord.ext.commands import Cog
from MusicBot.cogs.utils.voice import VoiceExtension from MusicBot.cogs.utils.voice import VoiceExtension
from MusicBot.cogs.utils.player import Player from MusicBot.cogs.utils.player import Player
from MusicBot.database.base import update, get_user, get_tracks_list
def setup(bot: discord.Bot): def setup(bot: discord.Bot):
bot.add_cog(Voice()) bot.add_cog(Voice())
@@ -38,10 +36,10 @@ class Voice(Cog, VoiceExtension):
return True return True
@toggle.command(name="menu", description="Toggle player menu.") @toggle.command(name="menu", description="Toggle player menu. Available only if you're the only one in the vocie channel.")
async def menu(self, ctx: discord.ApplicationContext) -> None: async def menu(self, ctx: discord.ApplicationContext) -> None:
if self.voice_check: if self.voice_check:
await ctx.respond("Меню", view=Player(ctx)) await ctx.respond("Меню", view=Player(ctx), ephemeral=True)
@voice.command(name="join", description="Join the voice channel you're currently in.") @voice.command(name="join", description="Join the voice channel you're currently in.")
async def join(self, ctx: discord.ApplicationContext) -> None: async def join(self, ctx: discord.ApplicationContext) -> None:
@@ -58,7 +56,7 @@ class Voice(Cog, VoiceExtension):
async def leave(self, ctx: discord.ApplicationContext) -> None: async def leave(self, ctx: discord.ApplicationContext) -> None:
vc = self.get_voice_client(ctx) vc = self.get_voice_client(ctx)
if await self.voice_check(ctx) and vc is not None: if await self.voice_check(ctx) and vc is not None:
await vc.disconnect() await vc.disconnect(force=True)
await ctx.respond("Отключение успешно!", delete_after=15, ephemeral=True) await ctx.respond("Отключение успешно!", delete_after=15, ephemeral=True)
@queue.command(description="Clear tracks queue.") @queue.command(description="Clear tracks queue.")
@@ -69,8 +67,8 @@ class Voice(Cog, VoiceExtension):
@queue.command(description="Get tracks queue.") @queue.command(description="Get tracks queue.")
async def get(self, ctx: discord.ApplicationContext) -> None: async def get(self, ctx: discord.ApplicationContext) -> None:
if await self.voice_check(ctx): if await self.voice_check(ctx):
user = get_user(ctx.user.id) guild = self.db.get_guild(ctx.guild.id)
tracks_list = user.get('tracks_list') tracks_list = guild.get('tracks_list')
embed = discord.Embed( embed = discord.Embed(
title='Список треков', title='Список треков',
color=discord.Color.dark_purple() color=discord.Color.dark_purple()
@@ -111,13 +109,14 @@ class Voice(Cog, VoiceExtension):
@track.command(description="Switch to the next song in the queue.") @track.command(description="Switch to the next song in the queue.")
async def next(self, ctx: discord.ApplicationContext) -> None: async def next(self, ctx: discord.ApplicationContext) -> None:
if await self.voice_check(ctx): if await self.voice_check(ctx):
uid = ctx.user.id gid = ctx.guild.id
tracks_list = get_tracks_list(uid) tracks_list = self.db.get_tracks_list(gid)
if not tracks_list: if not tracks_list:
await ctx.respond("Нет песенен в очереди.", delete_after=15, ephemeral=True) await ctx.respond("Нет песенен в очереди.", delete_after=15, ephemeral=True)
return return
update(uid, {'is_stopped': False}) self.db.update(gid, {'is_stopped': False})
title = await self.next_track(ctx) title = await self.next_track(ctx)
if title is not None: if title is not None:
await ctx.respond(f"Сейчас играет: **{title}**!", delete_after=15) await ctx.respond(f"Сейчас играет: **{title}**!", delete_after=15)
else:
await ctx.respond(f"Нет треков в очереди.", delete_after=15, ephemeral=True)