diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 89cabd4..3b7466d 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.10-bookworm +FROM python:3.11-bookworm WORKDIR /app diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 5ba3399..383df0a 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -13,7 +13,7 @@ services: volumes: # Update this to wherever you want VS Code to mount the folder of your project - - ..:/app:cached + - ..:/app # Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust. # cap_add: diff --git a/.github/workflows/python-unittest.yml b/.github/workflows/python-unittest.yml index f6fc6b1..c93a274 100644 --- a/.github/workflows/python-unittest.yml +++ b/.github/workflows/python-unittest.yml @@ -6,7 +6,7 @@ name: JukeBot CI Full on: push: branches: - - '**' + - 'main' pull_request: branches: - '**' @@ -19,7 +19,7 @@ jobs: strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11'] + python-version: ['3.9', '3.10', '3.11'] steps: - name: Checkout diff --git a/jukebot/abstract_components/abstract_collection.py b/jukebot/abstract_components/abstract_collection.py index 1e1bead..de8cf5a 100644 --- a/jukebot/abstract_components/abstract_collection.py +++ b/jukebot/abstract_components/abstract_collection.py @@ -27,7 +27,7 @@ def __getitem__(self, idx: int) -> _T: def __str__(self) -> str: return str(self.set) - def __add__(self, other: "AbstractCollection") -> "AbstractCollection": + def __add__(self, other: AbstractCollection) -> AbstractCollection: assert isinstance(other, AbstractCollection) self.set += other.set return self diff --git a/jukebot/abstract_components/abstract_service.py b/jukebot/abstract_components/abstract_service.py index c0112e3..65fa434 100644 --- a/jukebot/abstract_components/abstract_service.py +++ b/jukebot/abstract_components/abstract_service.py @@ -1,4 +1,11 @@ class AbstractService: + """This class allow you to implement your own service and using it from the bot. + To implement a service, create a new file in the `services` package then implement this class. + Don't forget, services are named `XXXService` where `XXX` is your action. + To register a service, go to the cog where it's used then in the setup function, add your service by using `bot.add_service(XXXService())`. + To use a service: `bot.services.XXX` where `XXX` is your previous named action. + """ + def __init__(self, bot): self.bot = bot diff --git a/jukebot/cogs/music.py b/jukebot/cogs/music.py index 88356b0..39b0f5b 100644 --- a/jukebot/cogs/music.py +++ b/jukebot/cogs/music.py @@ -3,10 +3,12 @@ from typing import Optional from urllib import parse -from disnake import CommandInteraction, Embed +from disnake import APISlashCommand, CommandInteraction, Embed, Forbidden from disnake.ext import commands -from disnake.ext.commands import Bot, BucketType +from disnake.ext.commands import BucketType +from jukebot import JukeBot +from jukebot.components.player import Player from jukebot.components.requests import ShazamRequest from jukebot.exceptions import QueryFailed from jukebot.services.music import ( @@ -26,7 +28,7 @@ class Music(commands.Cog): def __init__(self, bot): - self.bot: Bot = bot + self.bot: JukeBot = bot @commands.slash_command() @commands.cooldown(1, 5.0, BucketType.user) @@ -43,8 +45,18 @@ async def play(self, inter: CommandInteraction, query: str, top: Optional[bool] top : Optional[bool], optional Put the requested song at the top of the queue, by default False """ - with PlayService(self.bot) as ps: - await ps(interaction=inter, query=query, top=top) + if self.bot.players.get(inter.guild.id).is_playing: + # ? if player is playing, play command is just a shortcut for queue add command + await self.bot.get_slash_command("queue add").callback(self, inter, query, top) + return + + if not inter.response.is_done(): + await inter.response.defer() + + song, loop = await self.bot.services.play(interaction=inter, query=query, top=top) + + e: Embed = embed.music_message(song, loop) + await inter.edit_original_message(embed=e) @commands.slash_command() @commands.cooldown(1, 5.0, BucketType.user) @@ -59,8 +71,10 @@ async def leave(self, inter: CommandInteraction): inter : CommandInteraction The interaction """ - with LeaveService(self.bot) as ls: - await ls(interaction=inter) + await self.bot.services.leave(guild_id=inter.guild.id) + + e = embed.basic_message(title="Player disconnected") + await inter.send(embed=e) @commands.slash_command() @commands.cooldown(1, 5.0, BucketType.user) @@ -76,8 +90,10 @@ async def stop(self, inter: CommandInteraction): inter : CommandInteraction The interaction """ - with StopService(self.bot) as ss: - await ss(interaction=inter) + await self.bot.services.stop(guild_id=inter.guild.id) + + e = embed.basic_message(title="Player stopped") + await inter.send(embed=e) @commands.slash_command() @commands.cooldown(1, 5.0, BucketType.user) @@ -93,8 +109,10 @@ async def pause(self, inter: CommandInteraction): inter : CommandInteraction The interaction """ - with PauseService(self.bot) as ps: - await ps(interaction=inter) + await self.bot.services.pause(guild_id=inter.guild.id) + + e = embed.basic_message(title="Player paused") + await inter.send(embed=e) @commands.slash_command() @commands.cooldown(1, 5.0, BucketType.user) @@ -110,8 +128,23 @@ async def resume(self, inter: CommandInteraction): inter : CommandInteraction The interaction """ - with ResumeService(self.bot) as rs: - await rs(interaction=inter) + player: Player = self.bot.players[inter.guild.id] + if player.state.is_stopped and not player.queue.is_empty(): + # ? if player is stopped but queue isn't empty, resume the queue + await self.bot.get_slash_command("play").callback(self, inter, query="") + return + + ok = await self.bot.services.resume(guild_id=inter.guild.id) + + if ok: + e = embed.basic_message(title="Player resumed") + else: + cmd: APISlashCommand = self.bot.get_global_command_named("play") + e = embed.basic_message( + title="Nothing is currently playing", content=f"Try to add a music !" + ) + + await inter.send(embed=e) @commands.slash_command() @commands.cooldown(1, 5.0, BucketType.user) @@ -126,8 +159,17 @@ async def current(self, inter: CommandInteraction): inter : CommandInteraction The interaction """ - with CurrentSongService(self.bot) as css: - await css(inter) + song, stream, loop = await self.bot.services.current_song(guild_id=inter.guild.id) + + if stream and song: + e = embed.music_message(song, loop, stream.progress) + else: + cmd: APISlashCommand = self.bot.get_global_command_named("play") + e = embed.basic_message( + title="Nothing is currently playing", content=f"Try to add a music !" + ) + + await inter.send(embed=e) @commands.slash_command() @commands.cooldown(1, 5.0, BucketType.user) @@ -141,8 +183,12 @@ async def join(self, inter: CommandInteraction): inter : CommandInteraction The interaction """ - with JoinService(self.bot) as js: - await js(interaction=inter) + await self.bot.services.join(interaction=inter) + + e = embed.basic_message( + content=f"Connected to <#{inter.author.voice.channel.id}>\n" f"Bound to <#{inter.channel.id}>\n", + ) + await inter.send(embed=e) @commands.slash_command() @commands.cooldown(3, 10.0, BucketType.user) @@ -158,8 +204,10 @@ async def skip(self, inter: CommandInteraction): inter : CommandInteraction The interaction """ - with SkipService(self.bot) as ss: - await ss(interaction=inter) + await self.bot.services.skip(guild_id=inter.guild.id) + + e: embed = embed.basic_message(title="Skipped !") + await inter.send(embed=e) @commands.slash_command() @commands.check(checks.bot_is_playing) @@ -175,8 +223,18 @@ async def grab(self, inter: CommandInteraction): inter : CommandInteraction The interaction """ - with GrabService(self.bot) as gs: - await gs(interaction=inter) + song, stream = await self.bot.services.grab(guild_id=inter.guild.id) + + e = embed.grab_message(song, stream.progress) + e.add_field( + name="Voice channel", + value=f"`{inter.guild.name} — {inter.author.voice.channel.name}`", + ) + try: + await inter.author.send(embed=e) + await inter.send("Check your DMs!", ephemeral=True) + except Forbidden: + await inter.send("Your DMs are closed!", embed=e, ephemeral=True) @commands.slash_command() @commands.cooldown(1, 5.0, BucketType.user) @@ -199,8 +257,10 @@ async def loop( - queue (loop the current queue) - none (disable looping) """ - with LoopService(self.bot) as lp: - await lp(interaction=inter, mode=mode) + new_status = await self.bot.services.loop(guild_id=inter.guild.id, mode=mode) + + e: embed = embed.basic_message(title=new_status) + await inter.send(embed=e) @commands.slash_command() @commands.cooldown(1, 15.0, BucketType.guild) @@ -267,5 +327,16 @@ async def share(self, inter: CommandInteraction, url: str): await inter.edit_original_message(embed=e) -def setup(bot): +def setup(bot: JukeBot): bot.add_cog(Music(bot)) + + bot.add_service(GrabService(bot)) + bot.add_service(JoinService(bot)) + bot.add_service(LeaveService(bot)) + bot.add_service(LoopService(bot)) + bot.add_service(PauseService(bot)) + bot.add_service(PlayService(bot)) + bot.add_service(ResumeService(bot)) + bot.add_service(SkipService(bot)) + bot.add_service(StopService(bot)) + bot.add_service(CurrentSongService(bot)) diff --git a/jukebot/cogs/queue.py b/jukebot/cogs/queue.py index faa734d..6aa76fc 100644 --- a/jukebot/cogs/queue.py +++ b/jukebot/cogs/queue.py @@ -4,23 +4,26 @@ from disnake import CommandInteraction, Embed from disnake.ext import commands -from disnake.ext.commands import Bot, BucketType +from disnake.ext.commands import BucketType +from jukebot import JukeBot +from jukebot.components.requests.music_request import MusicRequest from jukebot.services.queue import ( AddService, ClearService, RemoveService, + ShowService, ShuffleService, ) from jukebot.utils import checks, embed if TYPE_CHECKING: - from jukebot.components import ResultSet + from jukebot.components import Result, ResultSet class Queue(commands.Cog): def __init__(self, bot): - self.bot: Bot = bot + self.bot: JukeBot = bot @commands.slash_command() async def queue(self, inter: CommandInteraction): @@ -44,7 +47,8 @@ async def show(self, inter: CommandInteraction): inter : CommandInteraction The interaction """ - queue: ResultSet = self.bot.players[inter.guild.id].queue + queue: ResultSet = await self.bot.services.show(guild_id=inter.guild.id) + e: Embed = embed.queue_message(queue, self.bot, title=f"Queue for {inter.guild.name}") await inter.send(embed=e) @@ -67,8 +71,16 @@ async def add( query: the URL or query to play top: Whether or not to put music at the top of the queue """ - with AddService(self.bot) as asr: - await asr(interaction=inter, query=query, top=top) + if not inter.response.is_done(): + await inter.response.defer() + + type, res = await self.bot.services.add(guild_id=inter.guild.id, author=inter.author, query=query, top=top) + + if type == MusicRequest.ResultType.PLAYLIST: + e: Embed = embed.basic_queue_message(content=f"Enqueued : {len(res)} songs") + else: + e: Embed = embed.result_enqueued(res) + await inter.edit_original_message(embed=e) @queue.sub_command() @commands.cooldown(1, 5.0, BucketType.user) @@ -76,57 +88,69 @@ async def add( @commands.check(checks.bot_and_user_in_same_channel) @commands.check(checks.bot_is_connected) @commands.check(checks.user_is_connected) - async def remove(self, inter: CommandInteraction, song: str): - """Remove a song from the queue + async def clear(self, inter: CommandInteraction): + """Remove all music from the current queue Parameters ---------- - inter: The interaction - song: The name of the music to remove. The bot auto completes the answer + inter : CommandInteraction + The interaction """ - with RemoveService(self.bot) as rs: - await rs(interaction=inter, song=song) # type:ignore + await self.bot.services.clear(guild_id=inter.guild.id) - @remove.autocomplete("song") - async def remove_autocomplete(self, inter: CommandInteraction, data: str): - data = data.lower() - queue: ResultSet = self.bot.players[inter.guild.id].queue - return [e.title for e in queue if data in e.title.lower()][:25] + e: Embed = embed.basic_message(title="The queue have been cleared.") + await inter.send(embed=e) @queue.sub_command() - @commands.cooldown(1, 5.0, BucketType.user) @commands.check(checks.bot_queue_is_not_empty) @commands.check(checks.bot_and_user_in_same_channel) @commands.check(checks.bot_is_connected) @commands.check(checks.user_is_connected) - async def clear(self, inter: CommandInteraction): - """Remove all music from the current queue + @commands.cooldown(1, 10.0, BucketType.guild) + async def shuffle(self, inter: CommandInteraction): + """Shuffles the current queue Parameters ---------- inter : CommandInteraction The interaction """ - with ClearService(self.bot) as cs: - await cs(interaction=inter) + await self.bot.services.shuffle(guild_id=inter.guild.id) + + e: Embed = embed.basic_message(title="Queue shuffled.") + await inter.send(embed=e) @queue.sub_command() + @commands.cooldown(1, 5.0, BucketType.user) @commands.check(checks.bot_queue_is_not_empty) @commands.check(checks.bot_and_user_in_same_channel) @commands.check(checks.bot_is_connected) @commands.check(checks.user_is_connected) - @commands.cooldown(1, 10.0, BucketType.guild) - async def shuffle(self, inter: CommandInteraction): - """Shuffles the current queue + async def remove(self, inter: CommandInteraction, song: str): + """Remove a song from the queue Parameters ---------- - inter : CommandInteraction - The interaction + inter: The interaction + song: The name of the music to remove. The bot auto completes the answer """ - with ShuffleService(self.bot) as ss: - await ss(interaction=inter) + elem: Result = await self.bot.services.remove(guild_id=inter.guild.id, song=song) + e: Embed = embed.basic_message(content=f"`{elem.title}` have been removed from the queue") + await inter.send(embed=e) -def setup(bot): + @remove.autocomplete("song") + async def remove_autocomplete(self, inter: CommandInteraction, data: str): + data = data.lower() + queue: ResultSet = self.bot.players[inter.guild.id].queue + return [e.title for e in queue if data in e.title.lower()][:25] + + +def setup(bot: JukeBot): bot.add_cog(Queue(bot)) + + bot.add_service(AddService(bot)) + bot.add_service(ClearService(bot)) + bot.add_service(RemoveService(bot)) + bot.add_service(ShuffleService(bot)) + bot.add_service(ShowService(bot)) diff --git a/jukebot/cogs/radio.py b/jukebot/cogs/radio.py index c9baa35..9e12f07 100644 --- a/jukebot/cogs/radio.py +++ b/jukebot/cogs/radio.py @@ -8,17 +8,16 @@ from disnake.ext.commands import BucketType from loguru import logger -from jukebot.services.music import PlayService +from jukebot import JukeBot from jukebot.utils import checks, converter, embed if TYPE_CHECKING: from disnake import Embed - from disnake.ext.commands import Bot class Radio(commands.Cog): def __init__(self, bot): - self.bot: Bot = bot + self.bot: JukeBot = bot self._radios: dict = {} async def cog_load(self) -> None: @@ -27,8 +26,7 @@ async def cog_load(self) -> None: async def _radio_process(self, inter: CommandInteraction, choices: list): query: str = random.choice(choices) logger.opt(lazy=True).debug(f"Choice is {query}") - with PlayService(self.bot) as ps: - await ps(interaction=inter, query=query, top=True) + await self.bot.services.play(interaction=inter, query=query, top=True) @commands.slash_command(description="Launch a random radio") @commands.cooldown(1, 5.0, BucketType.user) @@ -47,5 +45,5 @@ async def radio_autocomplete(self, inter: CommandInteraction, query: str): return [e for e in self._radios.keys() if query in e.lower()][:25] -def setup(bot): +def setup(bot: JukeBot): bot.add_cog(Radio(bot)) diff --git a/jukebot/cogs/search.py b/jukebot/cogs/search.py index c85bdfd..46ff331 100644 --- a/jukebot/cogs/search.py +++ b/jukebot/cogs/search.py @@ -5,13 +5,12 @@ from disnake import CommandInteraction from disnake.ext import commands -from disnake.ext.commands import Bot, BucketType +from disnake.ext.commands import BucketType from loguru import logger -from jukebot import components +from jukebot import JukeBot, components from jukebot.components.requests import SearchRequest from jukebot.exceptions import QueryCanceled, QueryFailed -from jukebot.services.music import PlayService from jukebot.utils import checks, embed from jukebot.views import SearchDropdownView, SearchInteraction @@ -21,7 +20,7 @@ class Search(commands.Cog): def __init__(self, bot): - self.bot: Bot = bot + self.bot: JukeBot = bot @commands.slash_command() @commands.max_concurrency(1, BucketType.user) @@ -32,6 +31,7 @@ async def search( inter: CommandInteraction, query: str, source: SearchRequest.Engine = SearchRequest.Engine.Youtube.value, + top: bool = False, ): """ Allows you to search the first 10 results for the desired music. @@ -41,6 +41,7 @@ async def search( inter: The interaction query: The query to search source: The website to use to search for the query + top: Whether or not put the result in top of the queue """ await inter.response.defer() logger.opt(lazy=True).debug( @@ -68,7 +69,9 @@ async def search( v = SearchDropdownView(inter.author, results) await inter.edit_original_message(embed=e, view=v) await v.wait() - await inter.edit_original_message(view=None) + + await inter.edit_original_message(embed=e, view=None) + result: str = v.result if result == SearchInteraction.CANCEL_TEXT: raise QueryCanceled("Search Canceled", query=query, full_query=f"{source}{query}") @@ -77,10 +80,10 @@ async def search( f"Query '{source}{query}' successful for guild '{inter.guild.name} (ID: {inter.guild.id})'." ) - with PlayService(self.bot) as ps: - func = ps(interaction=inter, query=result) + # ? we call play cause search is barely a shortcut for play with a search before + func = self.bot.get_slash_command("play").callback(self, inter, result, top) asyncio.ensure_future(func, loop=self.bot.loop) -def setup(bot): +def setup(bot: JukeBot): bot.add_cog(Search(bot)) diff --git a/jukebot/cogs/system.py b/jukebot/cogs/system.py index 72153c0..c57c2ab 100644 --- a/jukebot/cogs/system.py +++ b/jukebot/cogs/system.py @@ -9,6 +9,7 @@ from disnake.ext import commands from loguru import logger +from jukebot import JukeBot from jukebot.utils import Extensions, converter, embed ADMIN_GUILD_IDS = ( @@ -18,7 +19,7 @@ class System(commands.Cog): def __init__(self, bot): - self.bot = bot + self.bot: JukeBot = bot def _reload_all_cogs(self): logger.opt(lazy=True).info("Reloading all extensions.") @@ -145,5 +146,5 @@ async def commands(self, inter: CommandInteraction): await inter.send(file=File(data, "cmds.yaml"), ephemeral=True) -def setup(bot): +def setup(bot: JukeBot): bot.add_cog(System(bot)) diff --git a/jukebot/cogs/utility.py b/jukebot/cogs/utility.py index 2b24b3b..d217912 100644 --- a/jukebot/cogs/utility.py +++ b/jukebot/cogs/utility.py @@ -4,8 +4,9 @@ from disnake import CommandInteraction, InviteTarget from disnake.ext import commands -from disnake.ext.commands import Bot, BucketType +from disnake.ext.commands import BucketType +from jukebot import JukeBot from jukebot.services import ResetService from jukebot.utils import applications, checks, converter, embed from jukebot.views import ActivityView, PromoteView @@ -13,7 +14,7 @@ class Utility(commands.Cog): def __init__(self, bot): - self.bot: Bot = bot + self.bot: JukeBot = bot @commands.slash_command( description="Get information about the bot like the ping and the uptime.", @@ -99,9 +100,13 @@ async def reset(self, inter: CommandInteraction): inter : CommandInteraction The interaction """ - with ResetService(self.bot) as rs: - await rs(interaction=inter) + await self.bot.services.reset(guild=inter.guild) + e = embed.info_message(content="The player has been reset.") + await inter.send(embed=e) -def setup(bot): + +def setup(bot: JukeBot): bot.add_cog(Utility(bot)) + + bot.add_service(ResetService(bot)) diff --git a/jukebot/components/__init__.py b/jukebot/components/__init__.py index f930e8c..e813a42 100644 --- a/jukebot/components/__init__.py +++ b/jukebot/components/__init__.py @@ -1,5 +1,6 @@ from .audio_stream import AudioStream from .player import Player +from .playerset import PlayerSet from .result import Result from .resultset import ResultSet from .song import Song diff --git a/jukebot/components/player.py b/jukebot/components/player.py index 477ead7..a27095b 100644 --- a/jukebot/components/player.py +++ b/jukebot/components/player.py @@ -14,8 +14,6 @@ from jukebot.components.requests import StreamRequest from jukebot.components.resultset import ResultSet from jukebot.components.song import Song -from jukebot.services.music import LeaveService, PlayService -from jukebot.services.queue import AddService from jukebot.utils import coro @@ -69,8 +67,9 @@ def is_song_loop(self) -> bool: def is_queue_loop(self) -> bool: return self == Player.Loop.QUEUE - def __init__(self, bot: Bot): + def __init__(self, bot: Bot, guild_id: int): self.bot: Bot = bot + self._guild_id: int = guild_id self._voice: Optional[VoiceClient] = None self._stream: Optional[AudioStream] = None @@ -146,16 +145,14 @@ def _after(self, error): return if self._loop.is_queue_loop: - with AddService(self.bot) as ad: - func = ad(interaction=self.interaction, query=self.song.web_url, silent=True) + func = self.bot.add_service(guild_id=self._guild_id, author=self.song.requester, query=self.song.web_url) asyncio.ensure_future(func, loop=self.bot.loop) self._stream = None self._song = None if not self._queue.is_empty(): - with PlayService(self.bot) as ps: - func = ps(interaction=self.interaction, query="") + func = self.bot.get_slash_command("play").callback(self, self.interaction, query="") coro.run_threadsafe(func, self.bot.loop) return @@ -163,8 +160,7 @@ def _after(self, error): def _idle_callback(self) -> None: if not self._idle_task.cancelled(): - with LeaveService(self.bot) as ls: - func = ls(interaction=self.interaction) + func = self.bot.get_slash_command("leave").callback(self, self.interaction) asyncio.ensure_future(func, loop=self.bot.loop) def _set_idle_task(self) -> None: diff --git a/jukebot/components/playerset.py b/jukebot/components/playerset.py new file mode 100644 index 0000000..85160a6 --- /dev/null +++ b/jukebot/components/playerset.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from jukebot.abstract_components import AbstractMap +from jukebot.components.player import Player + +if TYPE_CHECKING: + from typing import List + + +class PlayerSet(AbstractMap[int, Player]): + _instance = None + + def __new__(cls, bot): + if cls._instance is None: + cls._instance = super(PlayerSet, cls).__new__(cls) + cls.bot = bot + return cls._instance + + def __getitem__(self, key): + if not key in self._collection: + self._collection[key] = Player(self.bot, guild_id=key) + return self._collection[key] + + def playing(self) -> List[Player]: + return [p for p in self._collection.values() if p.is_playing] diff --git a/jukebot/components/resultset.py b/jukebot/components/resultset.py index b157fb9..3e02673 100644 --- a/jukebot/components/resultset.py +++ b/jukebot/components/resultset.py @@ -2,7 +2,7 @@ import random from dataclasses import dataclass -from typing import List, Optional +from typing import List, Optional, Union from disnake import Member @@ -29,15 +29,22 @@ def empty(cls): def get(self) -> Result: return self.set.pop(0) - def put(self, result: Result) -> None: - self.set.append(result) + def put(self, result: Union[Result, ResultSet]) -> None: + if isinstance(result, ResultSet): + self.set += result + else: + self.set.append(result) - def add(self, result: Result) -> None: - self.set.insert(0, result) + def add(self, result: Union[Result, ResultSet]) -> None: + if isinstance(result, ResultSet): + self.set[0:0] = result[::-1] + else: + self.set.insert(0, result) def remove(self, elem: str) -> Optional[Result]: + elem = elem.lower() for i, e in enumerate(self.set): - if e.title.lower() == elem.lower(): + if e.title.lower() == elem: return self.set.pop(i) return None diff --git a/jukebot/exceptions/__init__.py b/jukebot/exceptions/__init__.py index 76de105..60c4849 100644 --- a/jukebot/exceptions/__init__.py +++ b/jukebot/exceptions/__init__.py @@ -1,2 +1,6 @@ -from .player_exception import PlayerConnexionException, PlayerException +from .player_exception import ( + PlayerConnexionException, + PlayerDontExistException, + PlayerException, +) from .query_exceptions import QueryCanceled, QueryException, QueryFailed diff --git a/jukebot/exceptions/player_exception.py b/jukebot/exceptions/player_exception.py index 5d016e5..047240b 100644 --- a/jukebot/exceptions/player_exception.py +++ b/jukebot/exceptions/player_exception.py @@ -8,3 +8,7 @@ def __init__(self, message: str) -> None: class PlayerConnexionException(PlayerException): pass + + +class PlayerDontExistException(PlayerException): + pass diff --git a/jukebot/jukebot.py b/jukebot/jukebot.py index bf4089a..193138d 100644 --- a/jukebot/jukebot.py +++ b/jukebot/jukebot.py @@ -3,20 +3,26 @@ import traceback from datetime import datetime from functools import cached_property -from typing import List from disnake.ext import commands from loguru import logger -from jukebot.abstract_components import AbstractMap -from jukebot.components import Player +from jukebot.abstract_components.abstract_map import AbstractMap +from jukebot.abstract_components.abstract_service import AbstractService +from jukebot.components import PlayerSet +from jukebot.utils import regex + + +class ServiceMap(AbstractMap[str, AbstractService]): + pass class JukeBot(commands.InteractionBot): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._start = datetime.now() - self._players: PlayerCollection = PlayerCollection(self) + self._players: PlayerSet = PlayerSet(self) + self._services: ServiceMap = ServiceMap() async def on_ready(self): logger.info(f"Logged in as {self.user} (ID: {self.user.id})") @@ -25,14 +31,32 @@ async def on_error(self, event, *args, **kwargs): logger.error(f"{event=}{args}{kwargs}") logger.error(f"{''.join(traceback.format_stack())}") + def add_service(self, service: AbstractService): + """Add service add a service to the bot. The services are in the services packages. + When a service is added, you can call it using `bot.services.`. + For example, if your service is called `PlayService`, then you can call it using `bot.services.play`. + + Parameters + ---------- + service : AbstractService + The service to add. Must implement `AbstractService`! + """ + name: str = regex.to_snake(service.__class__.__name__).replace("_service", "") + self.services[name] = service + setattr(self.services, name, service) + @property def start_time(self): return self._start @property - def players(self) -> "PlayerCollection": + def players(self) -> PlayerSet: return self._players + @property + def services(self) -> ServiceMap: + return self._services + @cached_property def members_count(self) -> int: return len(set(self.get_all_members())) @@ -40,21 +64,3 @@ def members_count(self) -> int: @cached_property def guilds_count(self) -> int: return len(self.guilds) - - -class PlayerCollection(AbstractMap[int, Player]): - _instance = None - - def __new__(cls, bot): - if cls._instance is None: - cls._instance = super(PlayerCollection, cls).__new__(cls) - cls.bot = bot - return cls._instance - - def __getitem__(self, key): - if not key in self._collection: - self._collection[key] = Player(self.bot) - return self._collection[key] - - def playing(self) -> List[Player]: - return [p for p in self._collection.values() if p.is_playing] diff --git a/jukebot/listeners/voice_handler.py b/jukebot/listeners/voice_handler.py index 4f4f4d1..22c126d 100644 --- a/jukebot/listeners/voice_handler.py +++ b/jukebot/listeners/voice_handler.py @@ -6,7 +6,6 @@ from disnake.ext import commands from disnake.ext.commands import Bot -from jukebot.services.music import PauseService, ResumeService if TYPE_CHECKING: from jukebot.components import Player @@ -35,16 +34,14 @@ async def on_voice_channel_connect(self, member: Member, channel: VoiceChannel): if self.bot.user in channel.members: player: Player = self.bot.players[channel.guild.id] if player.is_paused: - with ResumeService(self.bot) as rs: - await rs(interaction=player.interaction, silent=True) + await self.bot.services.resume(guild_id=channel.guild.id) @commands.Cog.listener() async def on_voice_channel_alone(self, member: Member, channel: VoiceChannel): if channel.members[0].id == self.bot.user.id: player: Player = self.bot.players[channel.guild.id] if player.is_playing: - with PauseService(self.bot) as rs: - await rs(interaction=player.interaction, silent=True) + await self.bot.services.pause(guild_id=channel.guild.id) def setup(bot): diff --git a/jukebot/services/music/current_song_service.py b/jukebot/services/music/current_song_service.py index 55f9004..661f1c0 100644 --- a/jukebot/services/music/current_song_service.py +++ b/jukebot/services/music/current_song_service.py @@ -2,25 +2,19 @@ from typing import TYPE_CHECKING -from disnake import CommandInteraction from jukebot.abstract_components import AbstractService -from jukebot.utils import embed if TYPE_CHECKING: from jukebot.components import AudioStream, Player, Song class CurrentSongService(AbstractService): - async def __call__(self, /, interaction: CommandInteraction): - player: Player = self.bot.players[interaction.guild.id] + async def __call__(self, /, guild_id: int): + player: Player = self.bot.players[guild_id] + stream: AudioStream = player.stream song: Song = player.song - if stream and song: - e = embed.music_message(song, player.loop, stream.progress) - else: - cmd: APISlashCommand = self.bot.get_global_command_named("play") - e = embed.basic_message( - title="Nothing is currently playing", content=f"Try to add a music !" - ) - await interaction.send(embed=e) + loop: Player.Loop = player.loop + + return song, stream, loop diff --git a/jukebot/services/music/grab_service.py b/jukebot/services/music/grab_service.py index 5d9b421..e03e84c 100644 --- a/jukebot/services/music/grab_service.py +++ b/jukebot/services/music/grab_service.py @@ -2,27 +2,17 @@ from typing import TYPE_CHECKING -from disnake import CommandInteraction, Forbidden from jukebot.abstract_components import AbstractService -from jukebot.utils import embed if TYPE_CHECKING: from jukebot.components import AudioStream, Player, Song class GrabService(AbstractService): - async def __call__(self, /, interaction: CommandInteraction): - player: Player = self.bot.players[interaction.guild.id] + async def __call__(self, /, guild_id: int): + player: Player = self.bot.players[guild_id] stream: AudioStream = player.stream song: Song = player.song - e = embed.grab_message(song, stream.progress) - e.add_field( - name="Voice channel", - value=f"`{interaction.guild.name} — {interaction.author.voice.channel.name}`", - ) - try: - await interaction.author.send(embed=e) - await interaction.send("Saved!", ephemeral=True) - except Forbidden: - await interaction.send("Your DMs are closed!", embed=e, ephemeral=True) + + return song, stream diff --git a/jukebot/services/music/join_service.py b/jukebot/services/music/join_service.py index 733f07b..874367f 100644 --- a/jukebot/services/music/join_service.py +++ b/jukebot/services/music/join_service.py @@ -1,13 +1,12 @@ from __future__ import annotations import copy -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from disnake import CommandInteraction from jukebot.abstract_components import AbstractService from jukebot.exceptions import PlayerConnexionException -from jukebot.utils import embed if TYPE_CHECKING: from jukebot.components import Player @@ -18,7 +17,6 @@ async def __call__( self, /, interaction: CommandInteraction, - silent: Optional[bool] = False, ): player: Player = self.bot.players[interaction.guild.id] try: @@ -26,17 +24,13 @@ async def __call__( except: # we remove the created player self.bot.players.pop(interaction.guild.id) + + cmd = self.bot.get_global_command_named("reset") raise PlayerConnexionException( f"Can't connect to **{interaction.author.voice.channel.name}**. " - f"Check both __bot__ and __channel__ permissions." - ) - - if not silent: - e = embed.basic_message( - content=f"Connected to <#{interaction.author.voice.channel.id}>\n" - f"Bound to <#{interaction.channel.id}>\n", + f"Check both __bot__ and __channel__ permissions.\n" + f"If the issue persists, try to reset your player with ." ) - await interaction.send(embed=e) cpy_inter = copy.copy(interaction) # time to trick the copied interaction @@ -47,4 +41,3 @@ async def __call__( cpy_inter.send = cpy_inter.edit_original_message = cpy_inter.channel.send player.interaction = cpy_inter - return True diff --git a/jukebot/services/music/leave_service.py b/jukebot/services/music/leave_service.py index a6b6f34..ea88b06 100644 --- a/jukebot/services/music/leave_service.py +++ b/jukebot/services/music/leave_service.py @@ -1,14 +1,10 @@ from __future__ import annotations -from disnake import CommandInteraction from jukebot.abstract_components import AbstractService -from jukebot.utils import embed class LeaveService(AbstractService): - async def __call__(self, /, interaction: CommandInteraction): + async def __call__(self, /, guild_id: int): # once the bot leave, we destroy is instance from the container - await self.bot.players.pop(interaction.guild.id).disconnect() - e = embed.basic_message(title="Player disconnected") - await interaction.send(embed=e) + await self.bot.players.pop(guild_id).disconnect() diff --git a/jukebot/services/music/loop_service.py b/jukebot/services/music/loop_service.py index c78c7ec..2ee482c 100644 --- a/jukebot/services/music/loop_service.py +++ b/jukebot/services/music/loop_service.py @@ -2,19 +2,18 @@ from typing import TYPE_CHECKING -from disnake import CommandInteraction from jukebot import components from jukebot.abstract_components import AbstractService -from jukebot.utils import embed if TYPE_CHECKING: from jukebot.components import Player class LoopService(AbstractService): - async def __call__(self, /, interaction: CommandInteraction, mode: str): - player: Player = self.bot.players[interaction.guild.id] + async def __call__(self, /, guild_id: int, mode: str): + # TODO: refactor to use enum str + player: Player = self.bot.players[guild_id] if mode == "song": player.loop = components.Player.Loop.SONG new_status = "Loop is set to song" @@ -25,5 +24,4 @@ async def __call__(self, /, interaction: CommandInteraction, mode: str): player.loop = components.Player.Loop.DISABLED new_status = "Loop is disabled" - e: embed = embed.basic_message(title=new_status) - await interaction.send(embed=e) + return new_status diff --git a/jukebot/services/music/pause_service.py b/jukebot/services/music/pause_service.py index de4033b..e801363 100644 --- a/jukebot/services/music/pause_service.py +++ b/jukebot/services/music/pause_service.py @@ -1,16 +1,10 @@ from __future__ import annotations -from typing import Optional -from disnake import CommandInteraction from jukebot.abstract_components import AbstractService -from jukebot.utils import embed class PauseService(AbstractService): - async def __call__(self, /, interaction: CommandInteraction, silent: Optional[bool] = False): - self.bot.players[interaction.guild.id].pause() - if not silent: - e = embed.basic_message(title="Player paused") - await interaction.send(embed=e) + async def __call__(self, /, guild_id: int): + self.bot.players[guild_id].pause() diff --git a/jukebot/services/music/play_service.py b/jukebot/services/music/play_service.py index d45a687..89fc8ef 100644 --- a/jukebot/services/music/play_service.py +++ b/jukebot/services/music/play_service.py @@ -2,16 +2,13 @@ from typing import TYPE_CHECKING, Optional -from disnake import CommandInteraction, Embed +from disnake import CommandInteraction from loguru import logger from jukebot import components from jukebot.abstract_components import AbstractService from jukebot.components.requests import StreamRequest -from jukebot.services import ResetService -from jukebot.services.music.join_service import JoinService -from jukebot.services.queue.add_service import AddService -from jukebot.utils import embed +from jukebot.exceptions.player_exception import PlayerConnexionException if TYPE_CHECKING: from jukebot.components import Player, Result, Song @@ -24,32 +21,23 @@ async def __call__( interaction: CommandInteraction, query: str, top: Optional[bool] = False, - silent: Optional[bool] = False, ): - if not interaction.response.is_done(): - await interaction.response.defer() - - # PlayerContainer create bot if needed player: Player = self.bot.players[interaction.guild.id] if not player.is_connected: - with JoinService(self.bot) as js: - if not await js(interaction=interaction, silent=True): - return False + await self.bot.services.join(interaction=interaction) if query: - with AddService(self.bot) as asr: - ok = await asr( - interaction=interaction, - query=query, - top=top, - silent=silent or not player.is_playing, - ) - if not ok: - return False + await self.bot.services.add( + guild_id=interaction.guild.id, + author=interaction.user, + query=query, + top=top, + ) if player.is_playing: - return True # return True bc everything is ok + # ? stop here cause it mean that we used play command as queue add command + return rqs: Result = player.queue.get() author = rqs.requester @@ -59,68 +47,22 @@ async def __call__( song: Song = components.Song(req.result) song.requester = author - for i in range(2): - if await self._try_to_play(interaction, player, song, i): - break - - with ResetService(self.bot) as rs, JoinService(self.bot) as js: - await rs(interaction=interaction, silent=True) - logger.opt(lazy=True).info(f"Server {interaction.guild.name} ({interaction.guild.id}) player reset.") - await js(interaction=interaction, silent=True) - logger.opt(lazy=True).info( - f"Server {interaction.guild.name} ({interaction.guild.id}) player connected." - ) - - else: - with ResetService(self.bot) as rs: - await rs(interaction=interaction, silent=True) + try: + await player.play(song) + except Exception as e: logger.opt(lazy=True).error( - f"Server {interaction.guild.name} ({interaction.guild.id}) can't play in its player after 2 attempts. " - f"Shouldn't happen." + f"Server {interaction.guild.name} ({interaction.guild.id}) can't play in its player. Err {e}" ) - e: Embed = embed.error_message( - content="The player cannot play on the voice channel. This is because he's not connected to a voice channel or he's already playing something.\n" - "This situation can happen when the player has been abruptly disconnected by Discord or a user. " - "Use the `reset` command to reset the player in this case.", + + cmd = self.bot.get_global_command_named("reset") + raise PlayerConnexionException( + "The player cannot play on the voice channel. This is because he's not connected to a voice channel or he's already playing something.\n" + "This situation can happen when the player has been abruptly disconnected by Discord or a user (kicked from a voice channel). " + f"Use the command to reset the player in this case." ) - await interaction.edit_original_message(embed=e) - return False logger.opt(lazy=True).success( f"Server {interaction.guild.name} ({interaction.guild.id}) can play in its player." ) - e: Embed = embed.music_message(song, player.loop) - await interaction.edit_original_message(embed=e) - return True - async def _try_to_play(self, interaction: CommandInteraction, player: Player, song: Song, attempt: int) -> bool: - """ - An asynchronous function to attempt to play a song in a Discord guild. - - Parameters - ---------- - interaction : CommandInteraction - The command interaction triggering the play attempt. - player : Player - The player responsible for playing the song. - song : Song - The song to be played. - attempt : int - The attempt number for playing the song. - - Returns - ------- - bool - True if the song was played successfully, False otherwise. - """ - logger.opt(lazy=True).info( - f"Attempt {attempt} to play in guild {interaction.guild.name} ({interaction.guild.id})" - ) - try: - await player.play(song) - except Exception as e: - logger.opt(lazy=True).error( - f"Server {interaction.guild.name} ({interaction.guild.id}) can't play in its player at attempt {attempt}. Err {e}" - ) - return False - return True + return song, player.loop diff --git a/jukebot/services/music/resume_service.py b/jukebot/services/music/resume_service.py index e688581..355e7d2 100644 --- a/jukebot/services/music/resume_service.py +++ b/jukebot/services/music/resume_service.py @@ -1,34 +1,20 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING -from disnake import APISlashCommand, CommandInteraction from jukebot.abstract_components import AbstractService -from jukebot.utils import embed -from .play_service import PlayService if TYPE_CHECKING: from jukebot.components import Player class ResumeService(AbstractService): - async def __call__(self, /, interaction: CommandInteraction, silent: Optional[bool] = False): - player: Player = self.bot.players[interaction.guild.id] + async def __call__(self, /, guild_id: int): + player: Player = self.bot.players[guild_id] if player.is_paused: player.resume() - if not silent: - e = embed.basic_message(title="Player resumed") - await interaction.send(embed=e) + return True - return - - if player.state.is_stopped and not player.queue.is_empty(): - with PlayService(self.bot) as play: - await play(interaction=interaction, query="") - return - - cmd: APISlashCommand = self.bot.get_global_command_named("play") - e = embed.basic_message(title="Nothing is currently playing", content=f"Try to add a music !") - await interaction.send(embed=e) + return False diff --git a/jukebot/services/music/skip_service.py b/jukebot/services/music/skip_service.py index 82e4b99..033c8b2 100644 --- a/jukebot/services/music/skip_service.py +++ b/jukebot/services/music/skip_service.py @@ -1,16 +1,10 @@ from __future__ import annotations -from typing import Optional -from disnake import CommandInteraction from jukebot.abstract_components import AbstractService -from jukebot.utils import embed class SkipService(AbstractService): - async def __call__(self, /, interaction: CommandInteraction, silent: Optional[bool] = False): - self.bot.players[interaction.guild.id].skip() - if not silent: - e: embed = embed.basic_message(title="Skipped !") - await interaction.send(embed=e) + async def __call__(self, /, guild_id: int): + self.bot.players[guild_id].skip() diff --git a/jukebot/services/music/stop_service.py b/jukebot/services/music/stop_service.py index b4bda3a..bc4ab30 100644 --- a/jukebot/services/music/stop_service.py +++ b/jukebot/services/music/stop_service.py @@ -1,13 +1,9 @@ from __future__ import annotations -from disnake import CommandInteraction from jukebot.abstract_components import AbstractService -from jukebot.utils import embed class StopService(AbstractService): - async def __call__(self, /, interaction: CommandInteraction): - self.bot.players[interaction.guild.id].stop() - e = embed.basic_message(title="Player stopped") - await interaction.send(embed=e) + async def __call__(self, /, guild_id: int): + self.bot.players[guild_id].stop() diff --git a/jukebot/services/queue/__init__.py b/jukebot/services/queue/__init__.py index 6581ff2..aa7822e 100644 --- a/jukebot/services/queue/__init__.py +++ b/jukebot/services/queue/__init__.py @@ -1,4 +1,5 @@ from .add_service import AddService from .clear_service import ClearService from .remove_service import RemoveService +from .show_service import ShowService from .shuffle_service import ShuffleService diff --git a/jukebot/services/queue/add_service.py b/jukebot/services/queue/add_service.py index 7569bc7..676148b 100644 --- a/jukebot/services/queue/add_service.py +++ b/jukebot/services/queue/add_service.py @@ -2,13 +2,12 @@ from typing import TYPE_CHECKING, Optional -from disnake import CommandInteraction, Embed +from disnake import Member from jukebot import components from jukebot.abstract_components import AbstractService from jukebot.components.requests.music_request import MusicRequest from jukebot.exceptions import QueryFailed -from jukebot.utils import embed if TYPE_CHECKING: from jukebot.components import Player, Result, ResultSet @@ -18,38 +17,28 @@ class AddService(AbstractService): async def __call__( self, /, - interaction: CommandInteraction, + guild_id: int, + author: Member, query: str, top: Optional[bool] = False, - silent: Optional[bool] = False, ): - if not interaction.response.is_done(): - await interaction.response.defer() - async with MusicRequest(query) as req: await req.execute() + if not req.success: raise QueryFailed(f"Nothing found for {query}", query=query, full_query=query) if req.type == MusicRequest.ResultType.PLAYLIST: - res: ResultSet = components.ResultSet.from_result(req.result, interaction.author) - player: Player = self.bot.players[interaction.guild.id] - if top: - player.queue = res + player.queue - else: - player.queue += res - - e: Embed = embed.basic_queue_message(title=f"Enqueued : {len(res)} songs") - await interaction.edit_original_message(embed=e) + res: ResultSet = components.ResultSet.from_result(req.result, author) + player: Player = self.bot.players[guild_id] else: res: Result = components.Result(req.result) - res.requester = interaction.author - player: Player = self.bot.players[interaction.guild.id] - if top: - player.queue.add(res) - else: - player.queue.put(res) - if not silent: - e: Embed = embed.result_enqueued(res) - await interaction.edit_original_message(embed=e) - return True + res.requester = author + player: Player = self.bot.players[guild_id] + + if top: + player.queue.add(res) + else: + player.queue.put(res) + + return req.type, res diff --git a/jukebot/services/queue/clear_service.py b/jukebot/services/queue/clear_service.py index cf568ef..c424bea 100644 --- a/jukebot/services/queue/clear_service.py +++ b/jukebot/services/queue/clear_service.py @@ -2,24 +2,19 @@ from typing import TYPE_CHECKING -from disnake import CommandInteraction, Embed from disnake.ext import commands from jukebot import components from jukebot.abstract_components import AbstractService -from jukebot.utils import embed if TYPE_CHECKING: from jukebot.components import Player class ClearService(AbstractService): - async def __call__(self, /, interaction: CommandInteraction): - player: Player = self.bot.players[interaction.guild.id] + async def __call__(self, /, guild_id: int): + player: Player = self.bot.players[guild_id] if player.loop == components.Player.Loop.QUEUE: raise commands.UserInputError("Can't clear queue when queue loop is enabled") player.queue = components.ResultSet.empty() - - e: Embed = embed.basic_message(title="The queue have been cleared.") - await interaction.send(embed=e) diff --git a/jukebot/services/queue/remove_service.py b/jukebot/services/queue/remove_service.py index 1e21006..45d745a 100644 --- a/jukebot/services/queue/remove_service.py +++ b/jukebot/services/queue/remove_service.py @@ -2,21 +2,18 @@ from typing import TYPE_CHECKING -from disnake import CommandInteraction, Embed from disnake.ext import commands from jukebot.abstract_components import AbstractService -from jukebot.utils import embed if TYPE_CHECKING: from jukebot.components import ResultSet class RemoveService(AbstractService): - async def __call__(self, /, interaction: CommandInteraction, song: str): - queue: ResultSet = self.bot.players[interaction.guild.id].queue + async def __call__(self, /, guild_id: int, song: str): + queue: ResultSet = self.bot.players[guild_id].queue if not (elem := queue.remove(song)): - raise commands.UserInputError(f"Can't delete item `{song}`") + raise commands.UserInputError(f"Can't delete song `{song}`. Not in playlist.") - e: Embed = embed.basic_message(content=f"`{elem.title}` have been removed from the queue") - await interaction.send(embed=e) + return elem diff --git a/jukebot/services/queue/show_service.py b/jukebot/services/queue/show_service.py new file mode 100644 index 0000000..96289ea --- /dev/null +++ b/jukebot/services/queue/show_service.py @@ -0,0 +1,9 @@ +from __future__ import annotations + + +from jukebot.abstract_components import AbstractService + + +class ShowService(AbstractService): + async def __call__(self, /, guild_id: int): + return self.bot.players[guild_id].queue diff --git a/jukebot/services/queue/shuffle_service.py b/jukebot/services/queue/shuffle_service.py index faa0f81..75da64f 100644 --- a/jukebot/services/queue/shuffle_service.py +++ b/jukebot/services/queue/shuffle_service.py @@ -1,13 +1,9 @@ from __future__ import annotations -from disnake import CommandInteraction, Embed from jukebot.abstract_components import AbstractService -from jukebot.utils import embed class ShuffleService(AbstractService): - async def __call__(self, /, interaction: CommandInteraction): - self.bot.players[interaction.guild.id].queue.shuffle() - e: Embed = embed.basic_message(title="Queue shuffled.") - await interaction.send(embed=e) + async def __call__(self, /, guild_id: int): + self.bot.players[guild_id].queue.shuffle() diff --git a/jukebot/services/reset_service.py b/jukebot/services/reset_service.py index 82da0a6..a0ebcf1 100644 --- a/jukebot/services/reset_service.py +++ b/jukebot/services/reset_service.py @@ -1,38 +1,27 @@ -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING -from disnake import CommandInteraction, Embed +from disnake import Guild from loguru import logger from jukebot.abstract_components import AbstractService -from jukebot.utils import embed +from jukebot.exceptions.player_exception import PlayerDontExistException if TYPE_CHECKING: from jukebot.components import Player class ResetService(AbstractService): - async def __call__(self, /, interaction: CommandInteraction, silent: Optional[bool] = False): - if not interaction.guild.id in self.bot.players: - logger.opt(lazy=True).debug( - f"Server {interaction.guild.name} ({interaction.guild.id}) try to kill a player that don't exist." - ) - if not silent: - e: Embed = embed.error_message(content="No player detected in this server.") - await interaction.send(embed=e, ephemeral=True) - return + async def __call__(self, /, guild: Guild): + if not guild.id in self.bot.players: + logger.opt(lazy=True).debug(f"Server {guild.name} ({guild.id}) try to kill a player that don't exist.") + raise PlayerDontExistException("No player detected in this server.") - player: Player = self.bot.players.pop(interaction.guild.id) + player: Player = self.bot.players.pop(guild.id) try: await player.disconnect(force=True) except Exception as e: logger.opt(lazy=True).error( - f"Error when force disconnecting the player of the guild {interaction.guild.name} ({interaction.guild.id}). " - f"Error: {e}" + f"Error when force disconnecting the player of the guild {guild.name} ({guild.id}). " f"Error: {e}" ) - logger.opt(lazy=True).success( - f"Server {interaction.guild.name} ({interaction.guild.id}) has successfully reset his player." - ) - if not silent: - e: Embed = embed.info_message(content="The player has been reset.") - await interaction.send(embed=e) + logger.opt(lazy=True).success(f"Server {guild.name} ({guild.id}) has successfully reset his player.") diff --git a/jukebot/utils/embed.py b/jukebot/utils/embed.py index 5d51167..1df3989 100644 --- a/jukebot/utils/embed.py +++ b/jukebot/utils/embed.py @@ -6,7 +6,7 @@ import disnake from disnake import APISlashCommand, Member -from disnake.ext.commands import Bot, Command +from disnake.ext.commands import Bot from jukebot.utils import converter @@ -138,7 +138,7 @@ def basic_queue_message(title="", content=""): embed: disnake.Embed = _base_embed(content=content, color=c) embed.set_author( name="Information" if title == "" else title, - icon_url="https://icons.iconarchive.com/icons/papirus-team/papirus-apps/512/logisim-icon-icon.png", + icon_url="https://cdn.icon-icons.com/icons2/1381/PNG/512/xt7playermpv_94294.png", ) return embed @@ -173,7 +173,7 @@ def result_enqueued(res: Result): embed.set_author( name=f"Enqueued : {res.title}", url=res.web_url, - icon_url="https://icons.iconarchive.com/icons/papirus-team/papirus-apps/512/logisim-icon-icon.png", + icon_url="https://cdn.icon-icons.com/icons2/1381/PNG/512/xt7playermpv_94294.png", ) embed.add_field(name="Channel", value=res.channel, inline=True) embed.add_field(name="Duration", value=res.fmt_duration) diff --git a/jukebot/utils/regex.py b/jukebot/utils/regex.py index 6ebed93..610625d 100644 --- a/jukebot/utils/regex.py +++ b/jukebot/utils/regex.py @@ -3,6 +3,14 @@ URL_REGEX = r"https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)" URL_PATTERN = re.compile(URL_REGEX, re.MULTILINE) +TO_SNAKE_REGEX = r"(? bool: return not URL_PATTERN.match(s) is None + + +def to_snake(pascal_str: str) -> str: + snake_str = re.sub(TO_SNAKE_PATTERN, r"_\1", pascal_str) + return snake_str.lower()