diff --git a/MusicBot/cogs/general.py b/MusicBot/cogs/general.py index 186bc08..d18d140 100644 --- a/MusicBot/cogs/general.py +++ b/MusicBot/cogs/general.py @@ -7,7 +7,7 @@ import yandex_music import yandex_music.exceptions 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 ( proccess_album, process_track, process_artist, ListenAlbum, ListenTrack, ListenArtist @@ -20,6 +20,7 @@ class General(Cog): def __init__(self, bot): self.bot = bot + self.db = BaseUsersDatabase() @discord.slash_command(description="Login to Yandex Music using access token.", guild_ids=[1247100229535141899]) @discord.option("token", type=discord.SlashCommandOptionType.string) @@ -32,8 +33,8 @@ class General(Cog): about = cast(yandex_music.Status, client.me).to_dict() uid = ctx.author.id - update(uid, {'ym_token': token}) - await ctx.respond(f'Привет, {about['account']['first_name']}!', ephemeral=True) + self.db.update(uid, {'ym_token': token}) + 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.option( @@ -52,8 +53,7 @@ class General(Cog): await ctx.respond('❌ Недопустимый тип.') return - token = get_ym_token(ctx.user.id) - + token = self.db.get_ym_token(ctx.user.id) if not token: await ctx.respond('❌ Необходимо указать свой токен доступа с помощью комманды /login.', delete_after=15, ephemeral=True) return @@ -68,13 +68,13 @@ class General(Cog): if content_type == 'album': album = result.albums.results[0] # type: ignore 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': track: yandex_music.Track = result.tracks.results[0] # type: ignore album_id = cast(int, track.albums[0].id) 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': artist = result.artists.results[0] # type: ignore 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) diff --git a/MusicBot/cogs/utils/find.py b/MusicBot/cogs/utils/find.py index 3914dd7..631396e 100644 --- a/MusicBot/cogs/utils/find.py +++ b/MusicBot/cogs/utils/find.py @@ -11,7 +11,7 @@ from discord.ui import View, Button, Item from discord import ButtonStyle, Interaction 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): @@ -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) VoiceExtension.__init__(self) self.album = album - + async def callback(self, interaction: Interaction) -> None: - if not interaction.user: + if not interaction.guild: return 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 track in volume: - add_track(interaction.user.id, track) + self.db.add_track(interaction.guild.id, track) - track = pop_track(interaction.user.id) - ym_track = yandex_music.Track(id=track['track_id'], title=track['title'], client=album.client) # type: ignore - title = await self.play_track(interaction, ym_track) + track = self.db.pop_track(interaction.guild.id) + ym_track = yandex_music.Track.de_json(track, client=album.client) # type: ignore + title = await self.play_track(interaction, ym_track) # type: ignore if title: await interaction.respond(f"Сейчас играет: **{album.title}**!", delete_after=15) else: @@ -81,7 +81,7 @@ class PlayAlbumButton(Button, VoiceExtension): 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) link_app = f"yandexmusic://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): - 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) link_app = f"yandexmusic://album/{album.id}" link_web = f"https://music.yandex.ru/album/{album.id}" @@ -107,7 +107,7 @@ class ListenAlbum(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) link_app = f"yandexmusic://artist/{artist_id}" link_web = f"https://music.yandex.ru/artist/{artist_id}" diff --git a/MusicBot/cogs/utils/player.py b/MusicBot/cogs/utils/player.py index 9f71939..0e40068 100644 --- a/MusicBot/cogs/utils/player.py +++ b/MusicBot/cogs/utils/player.py @@ -1,11 +1,13 @@ -from typing import cast - from discord.ui import View, Button, Item from discord import ButtonStyle, Interaction, ApplicationContext from MusicBot.cogs.utils.voice import VoiceExtension class PlayPauseButton(Button, VoiceExtension): + def __init__(self, **kwargs): + Button.__init__(self, **kwargs) + VoiceExtension.__init__(self) + async def callback(self, interaction: Interaction) -> None: vc = self.get_voice_client(interaction) if vc is not None: @@ -16,11 +18,24 @@ class PlayPauseButton(Button, VoiceExtension): self.resume_playing(interaction) 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: await self.next_track(interaction) 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): 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.play_pause_button = PlayPauseButton(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.prev_button) diff --git a/MusicBot/cogs/utils/voice.py b/MusicBot/cogs/utils/voice.py index e0cdba1..234ac98 100644 --- a/MusicBot/cogs/utils/voice.py +++ b/MusicBot/cogs/utils/voice.py @@ -7,13 +7,17 @@ from yandex_music import Track, ClientAsync import discord 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: + def __init__(self) -> None: + self.db = VoiceGuildsDatabase() + self.users_db = BaseUsersDatabase() + def clear_queue(self, ctx: ApplicationContext | Interaction): - if ctx.user: - update(ctx.user.id, {'tracks_list': []}) + if ctx.guild: + self.db.update(ctx.guild.id, {'tracks_list': []}) def get_voice_client(self, ctx: ApplicationContext | Interaction) -> discord.VoiceClient | None: """Return voice client for the given guild id. Return None if not present. @@ -30,8 +34,8 @@ class VoiceExtension: else: 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: """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. @@ -43,12 +47,11 @@ class VoiceExtension: Returns: str | None: Song title or None. """ - if not ctx.user: + if not ctx.guild: return vc = self.get_voice_client(ctx) if not vc: - await ctx.respond("Добавьте бота в голосовой канал при помощи команды /voice join.", delete_after=15, ephemeral=True) return if isinstance(ctx, Interaction): @@ -56,10 +59,10 @@ class VoiceExtension: else: loop = ctx.bot.loop - uid = ctx.user.id - user = get_user(uid) - if user.get('current_track') is not None: - add_track(uid, track) + gid = ctx.guild.id + guild = self.db.get_guild(gid) + if guild.get('current_track') is not None: + self.db.add_track(gid, track) await ctx.respond(f"Трек **{track.title}** был добавлен в очередь.", delete_after=15) else: 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)) - set_current_track(uid) - update(uid, {'is_stopped': False}) + self.db.set_current_track(gid, track) + self.db.update(gid, {'is_stopped': False}) return track.title def pause_playing(self, ctx: ApplicationContext | Interaction) -> None: - if not ctx.user: - return - vc = self.get_voice_client(ctx) if vc: vc.pause() def resume_playing(self, ctx: ApplicationContext | Interaction) -> None: - if not ctx.user: - return - vc = self.get_voice_client(ctx) if vc: vc.resume() def stop_playing(self, ctx: ApplicationContext | Interaction) -> None: - if not ctx.user: + if not ctx.guild: return vc = self.get_voice_client(ctx) 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() async def next_track(self, ctx: ApplicationContext | Interaction) -> str | None: @@ -106,22 +103,77 @@ class VoiceExtension: Returns: str | None: Track title or None. """ - if not ctx.user: + if not ctx.guild or not ctx.user: return - uid = ctx.user.id - user = get_user(uid) - if user.get('is_stopped'): + gid = ctx.guild.id + guild = self.db.get_guild(gid) + token = self.users_db.get_ym_token(ctx.user.id) + if guild.get('is_stopped'): return if not self.get_voice_client(ctx): # Silently return if bot got kicked return - tracks_list = user.get('tracks_list') - if tracks_list: - track = pop_track(uid) - ym_track = Track(id=track['track_id'], title=track['title'], client=ClientAsync(user.get('ym_token'))) # type: ignore + current_track = guild.get('current_track') + tracks_list = guild.get('tracks_list') + if tracks_list and current_track: + 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) - return await self.play_track(ctx, ym_track) - else: + return await self.play_track(ctx, ym_track) # type: ignore + elif current_track: 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 diff --git a/MusicBot/cogs/voice.py b/MusicBot/cogs/voice.py index 30ee9e2..130cb07 100644 --- a/MusicBot/cogs/voice.py +++ b/MusicBot/cogs/voice.py @@ -4,8 +4,6 @@ from discord.ext.commands import Cog from MusicBot.cogs.utils.voice import VoiceExtension from MusicBot.cogs.utils.player import Player -from MusicBot.database.base import update, get_user, get_tracks_list - def setup(bot: discord.Bot): bot.add_cog(Voice()) @@ -38,10 +36,10 @@ class Voice(Cog, VoiceExtension): 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: 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.") async def join(self, ctx: discord.ApplicationContext) -> None: @@ -58,7 +56,7 @@ class Voice(Cog, VoiceExtension): async def leave(self, ctx: discord.ApplicationContext) -> None: vc = self.get_voice_client(ctx) 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) @queue.command(description="Clear tracks queue.") @@ -69,8 +67,8 @@ class Voice(Cog, VoiceExtension): @queue.command(description="Get tracks queue.") async def get(self, ctx: discord.ApplicationContext) -> None: if await self.voice_check(ctx): - user = get_user(ctx.user.id) - tracks_list = user.get('tracks_list') + guild = self.db.get_guild(ctx.guild.id) + tracks_list = guild.get('tracks_list') embed = discord.Embed( title='Список треков', color=discord.Color.dark_purple() @@ -111,13 +109,14 @@ class Voice(Cog, VoiceExtension): @track.command(description="Switch to the next song in the queue.") async def next(self, ctx: discord.ApplicationContext) -> None: if await self.voice_check(ctx): - uid = ctx.user.id - tracks_list = get_tracks_list(uid) + gid = ctx.guild.id + tracks_list = self.db.get_tracks_list(gid) if not tracks_list: await ctx.respond("Нет песенен в очереди.", delete_after=15, ephemeral=True) return - update(uid, {'is_stopped': False}) + self.db.update(gid, {'is_stopped': False}) title = await self.next_track(ctx) if title is not None: await ctx.respond(f"Сейчас играет: **{title}**!", delete_after=15) - \ No newline at end of file + else: + await ctx.respond(f"Нет треков в очереди.", delete_after=15, ephemeral=True)