diff --git a/data/radios.yaml b/data/radios.yaml index fdde2d5..04053f9 100644 --- a/data/radios.yaml +++ b/data/radios.yaml @@ -1,10 +1,10 @@ -- lofi: +lofi: - "https://www.youtube.com/watch?v=jfKfPfyJRdk" - "https://www.youtube.com/watch?v=rUxyKA_-grg" - "https://www.youtube.com/watch?v=HAZoLuME-PU" - "https://www.youtube.com/watch?v=7NOSDKb0HlU" -- jazz: +jazz: - "https://www.youtube.com/watch?v=Dx5qFachd3A" - "https://www.youtube.com/watch?v=fEvM-OUbaKs" - "https://www.youtube.com/watch?v=g06AjrOlki0" @@ -12,27 +12,27 @@ - "https://www.youtube.com/watch?v=c3IVTi6TlfE" - "https://www.youtube.com/watch?v=_rMZt292mJc" -- rocknroll: +rocknroll: - "https://www.youtube.com/watch?v=zdBGqWnpDRk" - "https://www.youtube.com/playlist?list=PLZN_exA7d4RVmCQrG5VlWIjMOkMFZVVOc" -- chiptune: +chiptune: - "https://www.youtube.com/watch?v=KzFmtxFG9z4" - "https://www.youtube.com/watch?v=VcQqVMQHYYU&pp=ygUOY2hpcHR1bmUgcmFkaW8%3D" -- synthwave: +synthwave: - "https://www.youtube.com/playlist?list=PL9a7fFpVuuJCNx1VfUKlusNP-4TGBTuz5" - "https://www.youtube.com/watch?v=Ss3DEK1rGuM" - "https://www.youtube.com/watch?v=FvwJ_Fh5_uE" - "https://www.youtube.com/watch?v=4xDzrJKXOOY" -- lounge: +lounge: - "https://www.youtube.com/watch?v=fEvM-OUbaKs" - "https://www.youtube.com/watch?v=Dx5qFachd3A" - "https://www.youtube.com/watch?v=XQuR1OxYJt0" - "https://www.youtube.com/watch?v=3XbEUv_MCj0" -- minecraft: +minecraft: - "https://www.youtube.com/watch?v=Pa_s7ogtokM" - "https://www.youtube.com/watch?v=TsTtqGAxvWk" - "https://www.youtube.com/watch?v=Dg0IjOzopYU" @@ -40,29 +40,29 @@ - "https://www.youtube.com/watch?v=0KvlwMd3C4Y" - "https://www.youtube.com/playlist?list=PL3817D41C7D841E23" -- rap: +rap: - "https://www.youtube.com/watch?v=05689ErDUdM" - "https://www.youtube.com/watch?v=0MOkLkTP-Jk" -- phonk: +phonk: - https://www.youtube.com/watch?v=S6helKOW5P0 - https://www.youtube.com/watch?v=8v_kKMaq5po -- poprock: +poprock: - "https://www.youtube.com/watch?v=EurKD84TFtA" - "https://www.youtube.com/watch?v=uRImXboQnzE" - "https://www.youtube.com/watch?v=Va-h6WZPUzQ" -- workout: +workout: - "https://www.youtube.com/watch?v=qWf-FPFmVw0" - "https://www.youtube.com/watch?v=fBnpWP4Fneg" - "https://www.youtube.com/watch?v=bT408U-LOn8" -- trap: +trap: - "https://www.youtube.com/watch?v=EA-6o1_vrsA" - "https://www.youtube.com/watch?v=-cCR-oqsLRQ" - https://www.youtube.com/watch?v=EapVttArmsE -- slowed: +slowed: - "https://www.youtube.com/playlist?list=PLF_ZnpSKNQQFTeHaQ1ZR5l5cx5nRiD80w" - "https://www.youtube.com/playlist?list=PLsmLp2JHrigK1FBB-nuy3panZJzH5sHWO" diff --git a/jukebot/abstract_components/__init__.py b/jukebot/abstract_components/__init__.py index 52293d2..9d35f52 100644 --- a/jukebot/abstract_components/__init__.py +++ b/jukebot/abstract_components/__init__.py @@ -1,4 +1,3 @@ -from .abstract_cache import AbstractCache as AbstractCache from .abstract_collection import AbstractCollection as AbstractCollection from .abstract_map import AbstractMap as AbstractMap from .abstract_request import AbstractRequest as AbstractRequest diff --git a/jukebot/abstract_components/abstract_cache.py b/jukebot/abstract_components/abstract_cache.py deleted file mode 100644 index b3d5042..0000000 --- a/jukebot/abstract_components/abstract_cache.py +++ /dev/null @@ -1,39 +0,0 @@ -from __future__ import annotations - -from collections import abc -from typing import ClassVar, TypeVar - -_T = TypeVar("_T") -_V = TypeVar("_V") - - -class AbstractCache[T, V](abc.MutableSequence): - _cache: ClassVar[dict] = {} - - def __setitem__(self, k: _T, v: _V) -> None: - self._cache[k] = v - - def __contains__(self, k: _T): - try: - self._cache[k] - except KeyError: - return False - return True - - def __delitem__(self, k: _T) -> None: - del self._cache[k] - - def __getitem__(self, k: _T) -> _V: - return self._cache[k] - - def __len__(self) -> int: - return len(self._cache) - - def __str__(self) -> str: - return str(self._cache) - - def clear(self) -> None: - self._cache = {} - - def insert(self, index: _T, value: _V) -> None: - self.__setitem__(index, value) diff --git a/jukebot/abstract_components/abstract_collection.py b/jukebot/abstract_components/abstract_collection.py index 62a8cad..752f40d 100644 --- a/jukebot/abstract_components/abstract_collection.py +++ b/jukebot/abstract_components/abstract_collection.py @@ -1,27 +1,27 @@ from __future__ import annotations from collections import abc -from collections.abc import Iterator from dataclasses import dataclass -from typing import TypeVar +from typing import TYPE_CHECKING -_T = TypeVar("_T") +if TYPE_CHECKING: + from collections.abc import Iterator @dataclass class AbstractCollection[T](abc.Collection): - set: list[_T] + set: list[T] def __len__(self) -> int: return len(self.set) - def __iter__(self) -> Iterator[_T]: + def __iter__(self) -> Iterator[T]: yield from self.set def __contains__(self, e: object) -> bool: return e in self.set - def __getitem__(self, idx: int) -> _T: + def __getitem__(self, idx: int) -> T: return self.set[idx] def __str__(self) -> str: diff --git a/jukebot/abstract_components/abstract_map.py b/jukebot/abstract_components/abstract_map.py index 48716c3..8c66ac4 100644 --- a/jukebot/abstract_components/abstract_map.py +++ b/jukebot/abstract_components/abstract_map.py @@ -1,35 +1,36 @@ from __future__ import annotations from collections import abc -from collections.abc import Iterator -from typing import TypeVar +from typing import TYPE_CHECKING -_T = TypeVar("_T") -_V = TypeVar("_V") +if TYPE_CHECKING: + from collections.abc import Iterator class AbstractMap[T, V](abc.MutableMapping): - def __new__(cls): + _collection: dict[T, V] + + def __new__(cls, *args, **kwargs): instance = super().__new__(cls) instance._collection = {} return instance - def __setitem__(self, k: _T, v: _V) -> None: + def __setitem__(self, k: T, v: V) -> None: self._collection[k] = v - def __contains__(self, k: _T): + def __contains__(self, k: object) -> bool: return k in self._collection - def __delitem__(self, k: _T) -> None: + def __delitem__(self, k: T) -> None: del self._collection[k] - def __getitem__(self, k: _T) -> _V: + def __getitem__(self, k: T) -> V: return self._collection[k] def __len__(self) -> int: return len(self._collection) - def __iter__(self) -> Iterator[_T]: + def __iter__(self) -> Iterator[T]: yield from self._collection def __str__(self) -> str: diff --git a/jukebot/abstract_components/abstract_service.py b/jukebot/abstract_components/abstract_service.py index 65fa434..54e870c 100644 --- a/jukebot/abstract_components/abstract_service.py +++ b/jukebot/abstract_components/abstract_service.py @@ -1,3 +1,11 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from jukebot.jukebot import JukeBot + + 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. @@ -6,7 +14,7 @@ class AbstractService: To use a service: `bot.services.XXX` where `XXX` is your previous named action. """ - def __init__(self, bot): + def __init__(self, bot: JukeBot): self.bot = bot async def __call__(self, *args, **kwargs): diff --git a/jukebot/cogs/music.py b/jukebot/cogs/music.py index 040e8a4..0cd082d 100644 --- a/jukebot/cogs/music.py +++ b/jukebot/cogs/music.py @@ -1,12 +1,13 @@ from __future__ import annotations +from typing import TYPE_CHECKING from urllib import parse -from disnake import APISlashCommand, CommandInteraction, Embed, Forbidden +from disnake import CommandInteraction, Forbidden from disnake.ext import commands from disnake.ext.commands import BucketType -from jukebot import JukeBot +from jukebot.components import Song from jukebot.components.player import Player from jukebot.components.requests import ShazamRequest from jukebot.exceptions import QueryFailed @@ -22,7 +23,16 @@ SkipService, StopService, ) -from jukebot.utils import aioweb, checks, embed, regex +from jukebot.utils import aioweb, checks, regex +from jukebot.views.container import basic_message as c_basic_message +from jukebot.views.embed import basic_message, grab_message, music_found_message, music_message, share_message + +if TYPE_CHECKING: + from datetime import timedelta + + from disnake import APISlashCommand, Embed, ui + + from jukebot import JukeBot class Music(commands.Cog): @@ -54,7 +64,7 @@ async def play(self, inter: CommandInteraction, query: str, top: bool | None = F song, loop = await self.bot.services.play(interaction=inter, query=query, top=top) - e: Embed = embed.music_message(song, loop) + e: Embed = music_message(song, loop) await inter.edit_original_message(embed=e) @commands.slash_command() @@ -70,10 +80,11 @@ async def leave(self, inter: CommandInteraction): inter : CommandInteraction The interaction """ - await self.bot.services.leave(guild_id=inter.guild.id) + duration: timedelta = await self.bot.services.leave(guild_id=inter.guild.id) + d = str(duration).split(".")[0] # ? remove the microseconds - e = embed.basic_message(title="Player disconnected") - await inter.send(embed=e) + c = c_basic_message(title="Player disconnected", content=f"Session duration : `{d}`") + await inter.send(components=c) @commands.slash_command() @commands.cooldown(1, 5.0, BucketType.user) @@ -91,8 +102,8 @@ async def stop(self, inter: CommandInteraction): """ await self.bot.services.stop(guild_id=inter.guild.id) - e = embed.basic_message(title="Player stopped") - await inter.send(embed=e) + c = c_basic_message(title="Player stopped") + await inter.send(components=c) @commands.slash_command() @commands.cooldown(1, 5.0, BucketType.user) @@ -110,8 +121,8 @@ async def pause(self, inter: CommandInteraction): """ await self.bot.services.pause(guild_id=inter.guild.id) - e = embed.basic_message(title="Player paused") - await inter.send(embed=e) + c = c_basic_message(title="Player paused") + await inter.send(components=c) @commands.slash_command() @commands.cooldown(1, 5.0, BucketType.user) @@ -136,12 +147,10 @@ async def resume(self, inter: CommandInteraction): ok = await self.bot.services.resume(guild_id=inter.guild.id) if ok: - e = embed.basic_message(title="Player resumed") + e = 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 !" - ) + e = basic_message(title="Nothing is currently playing", content=f"Try to add a music !") await inter.send(embed=e) @@ -161,12 +170,10 @@ async def current(self, inter: CommandInteraction): 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) + e = 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 !" - ) + e = basic_message(title="Nothing is currently playing", content=f"Try to add a music !") await inter.send(embed=e) @@ -184,10 +191,18 @@ async def join(self, inter: CommandInteraction): """ await self.bot.services.join(interaction=inter) - e = embed.basic_message( - content=f"Connected to <#{inter.author.voice.channel.id}>\nBound to <#{inter.channel.id}>\n", + cmd: APISlashCommand = self.bot.get_global_command_named("play") + + c = c_basic_message( + title="Connected!", + content=[ + f"Playing in <#{inter.author.voice.channel.id}>", + f"Bound to <#{inter.channel.id}>", + "", + f"Use to start listening to something", + ], ) - await inter.send(embed=e) + await inter.send(components=c) @commands.slash_command() @commands.cooldown(3, 10.0, BucketType.user) @@ -204,9 +219,11 @@ async def skip(self, inter: CommandInteraction): The interaction """ await self.bot.services.skip(guild_id=inter.guild.id) + song, *_ = await self.bot.services.current_song(guild_id=inter.guild.id) + assert isinstance(song, Song) - e: embed = embed.basic_message(title="Skipped !") - await inter.send(embed=e) + c: ui.Container = c_basic_message(title="Skipped!", content=f"`{song.title}` has been skipped") + await inter.send(components=c) @commands.slash_command() @commands.check(checks.bot_is_playing) @@ -224,11 +241,12 @@ async def grab(self, inter: CommandInteraction): """ song, stream = await self.bot.services.grab(guild_id=inter.guild.id) - e = embed.grab_message(song, stream.progress) + e = 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) @@ -243,7 +261,7 @@ async def grab(self, inter: CommandInteraction): async def loop( self, inter: CommandInteraction, - mode: commands.option_enum(["song", "queue", "none"]), + mode: commands.option_enum(Player.Loop.values()), ): """ Allow user to enable or disable the looping of a song or queue. @@ -256,10 +274,14 @@ async def loop( - queue (loop the current queue) - none (disable looping) """ - new_status = await self.bot.services.loop(guild_id=inter.guild.id, mode=mode) + old, new = 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) + c: ui.Container = c_basic_message( + title=f"Player loop is now set to __{new}__!", + content=f"It was previously set to `{old}`", + ) + + await inter.send(components=c) @commands.slash_command() @commands.cooldown(1, 15.0, BucketType.guild) @@ -284,7 +306,7 @@ async def find(self, inter: CommandInteraction, url: str): if not req.success: raise QueryFailed("No music found for this media..", query="", full_query=url) - e: Embed = embed.music_found_message(req.result) + e: Embed = music_found_message(req.result) await inter.edit_original_message(embed=e) @commands.slash_command() @@ -316,7 +338,8 @@ async def share(self, inter: CommandInteraction, url: str): title: str = data["entitiesByUniqueId"][data["entityUniqueId"]].get("title", "Unknown title") artist: str = data["entitiesByUniqueId"][data["entityUniqueId"]].get("artistName", "Unknown artist") img: str = data["entitiesByUniqueId"][data["entityUniqueId"]].get("thumbnailUrl", "") - e: Embed = embed.share_message( + + e: Embed = share_message( inter.author, title=f"{artist} - {title}", content=content, diff --git a/jukebot/cogs/queue.py b/jukebot/cogs/queue.py index 81e3dfd..a10595a 100644 --- a/jukebot/cogs/queue.py +++ b/jukebot/cogs/queue.py @@ -2,11 +2,10 @@ from typing import TYPE_CHECKING -from disnake import CommandInteraction, Embed +from disnake import CommandInteraction from disnake.ext import commands from disnake.ext.commands import BucketType -from jukebot import JukeBot from jukebot.components.requests.music_request import MusicRequest from jukebot.services.queue import ( AddService, @@ -15,9 +14,13 @@ ShowService, ShuffleService, ) -from jukebot.utils import checks, embed +from jukebot.utils import checks +from jukebot.views.embed import VOID_TOKEN, basic_message, basic_queue_message, queue_message, result_enqueued if TYPE_CHECKING: + from disnake import Embed + + from jukebot import JukeBot from jukebot.components import Result, ResultSet @@ -48,8 +51,15 @@ async def show(self, inter: CommandInteraction): The interaction """ queue: ResultSet = await self.bot.services.show(guild_id=inter.guild.id) + e: Embed = queue_message(queue, title=f"Queue for {inter.guild.name}") + + cmd = self.bot.get_global_command_named("queue") + e.add_field( + name=VOID_TOKEN, + value=f"Use command or to add or remove a song.", + inline=False, + ) - e: Embed = embed.queue_message(queue, self.bot, title=f"Queue for {inter.guild.name}") await inter.send(embed=e) @queue.sub_command() @@ -77,9 +87,9 @@ async def add( 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") + e: Embed = basic_queue_message(content=f"Enqueued : {len(res)} songs") else: - e: Embed = embed.result_enqueued(res) + e: Embed = result_enqueued(res) await inter.edit_original_message(embed=e) @queue.sub_command() @@ -98,7 +108,7 @@ async def clear(self, inter: CommandInteraction): """ await self.bot.services.clear(guild_id=inter.guild.id) - e: Embed = embed.basic_message(title="The queue have been cleared.") + e: Embed = basic_message(title="The queue have been cleared.") await inter.send(embed=e) @queue.sub_command() @@ -117,7 +127,7 @@ async def shuffle(self, inter: CommandInteraction): """ await self.bot.services.shuffle(guild_id=inter.guild.id) - e: Embed = embed.basic_message(title="Queue shuffled.") + e: Embed = basic_message(title="Queue shuffled.") await inter.send(embed=e) @queue.sub_command() @@ -136,7 +146,7 @@ async def remove(self, inter: CommandInteraction, song: str): """ 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") + e: Embed = basic_message(content=f"`{elem.title}` have been removed from the queue") await inter.send(embed=e) @remove.autocomplete("song") diff --git a/jukebot/cogs/radio.py b/jukebot/cogs/radio.py index b9cc199..39a049e 100644 --- a/jukebot/cogs/radio.py +++ b/jukebot/cogs/radio.py @@ -3,17 +3,21 @@ import random from typing import TYPE_CHECKING +import yaml +from aiofiles import open as aiopen from disnake import CommandInteraction from disnake.ext import commands from disnake.ext.commands import BucketType from loguru import logger -from jukebot import JukeBot -from jukebot.utils import checks, converter, embed +from jukebot.utils import checks +from jukebot.views.embed import error_message if TYPE_CHECKING: from disnake import Embed + from jukebot import JukeBot + class Radio(commands.Cog): def __init__(self, bot): @@ -21,7 +25,8 @@ def __init__(self, bot): self._radios: dict = {} async def cog_load(self) -> None: - self._radios = converter.radios_yaml_to_dict() + with aiopen("./data/radios.yaml", "r") as f: + self._radios = yaml.safe_load(f) async def _radio_process(self, inter: CommandInteraction, choices: list): query: str = random.choice(choices) @@ -34,7 +39,7 @@ async def _radio_process(self, inter: CommandInteraction, choices: list): async def radio(self, inter: CommandInteraction, radio: str): choices: list = self._radios.get(radio, []) if not choices: - e: Embed = embed.error_message(content=f"No radio found with the name `{radio}`") + e: Embed = error_message(content=f"No radio found with the name `{radio}`") await inter.send(embed=e) return diff --git a/jukebot/cogs/search.py b/jukebot/cogs/search.py index 46ff331..224cf46 100644 --- a/jukebot/cogs/search.py +++ b/jukebot/cogs/search.py @@ -8,13 +8,15 @@ from disnake.ext.commands import BucketType from loguru import logger -from jukebot import JukeBot, components +from jukebot import components from jukebot.components.requests import SearchRequest from jukebot.exceptions import QueryCanceled, QueryFailed -from jukebot.utils import checks, embed +from jukebot.utils import checks from jukebot.views import SearchDropdownView, SearchInteraction +from jukebot.views.embed import search_result_message if TYPE_CHECKING: + from jukebot import JukeBot from jukebot.components import ResultSet @@ -64,7 +66,7 @@ async def search( if not results: raise QueryFailed(f"Nothing found for {query}", query=query, full_query=f"{source}{query}") - e = embed.search_result_message(playlist=results, title=f"Result for {query}") + e = search_result_message(playlist=results, title=f"Result for {query}") v = SearchDropdownView(inter.author, results) await inter.edit_original_message(embed=e, view=v) diff --git a/jukebot/cogs/system.py b/jukebot/cogs/system.py index 2ecf57c..03c0a99 100644 --- a/jukebot/cogs/system.py +++ b/jukebot/cogs/system.py @@ -3,13 +3,17 @@ import io import os from datetime import datetime +from typing import TYPE_CHECKING from disnake import CommandInteraction, File from disnake.ext import commands from loguru import logger -from jukebot import JukeBot -from jukebot.utils import Extensions, converter, embed +from jukebot.utils import Extensions, converter +from jukebot.views.embed import VOID_TOKEN, info_message + +if TYPE_CHECKING: + from jukebot import JukeBot ADMIN_GUILD_IDS = ( list(map(int, os.environ["BOT_ADMIN_GUILD_IDS"].split(","))) if "BOT_ADMIN_GUILD_IDS" in os.environ else [] @@ -92,7 +96,7 @@ async def stats(self, inter: CommandInteraction): inter : CommandInteraction The interaction """ - e = embed.info_message(title=f"Stats about {self.bot.user.name}") + e = info_message(title=f"Stats about {self.bot.user.name}") e.add_field(name="📡 Ping", value=f"┕`{self.bot.latency * 1000:.2f}ms`") uptime = datetime.now() - self.bot.start_time days, hours, minutes, seconds = converter.seconds_to_time(int(uptime.total_seconds())) @@ -100,13 +104,13 @@ async def stats(self, inter: CommandInteraction): name="⏱ Uptime", value=f"┕`{days}d, {hours}h, {minutes}m, {seconds}s`", ) - e.add_field(name=embed.VOID_TOKEN, value=embed.VOID_TOKEN) + e.add_field(name=VOID_TOKEN, value=VOID_TOKEN) e.add_field(name="🏛️ Servers", value=f"┕`{len(self.bot.guilds)}`", inline=True) e.add_field( name="👥 Members", value=f"┕`{len(set(self.bot.get_all_members()))}`", ) - e.add_field(name=embed.VOID_TOKEN, value=embed.VOID_TOKEN) + e.add_field(name=VOID_TOKEN, value=VOID_TOKEN) e.add_field( name="📻 Players created", value=f"┕`{len(self.bot.players)}`", @@ -115,7 +119,7 @@ async def stats(self, inter: CommandInteraction): name="🎶 Players playing", value=f"┕`{len(self.bot.players.playing())}`", ) - e.add_field(name=embed.VOID_TOKEN, value=embed.VOID_TOKEN) + e.add_field(name=VOID_TOKEN, value=VOID_TOKEN) await inter.send(embed=e, ephemeral=True) diff --git a/jukebot/cogs/utility.py b/jukebot/cogs/utility.py index 1b588f0..b1c0df4 100644 --- a/jukebot/cogs/utility.py +++ b/jukebot/cogs/utility.py @@ -1,15 +1,19 @@ from __future__ import annotations from datetime import datetime +from typing import TYPE_CHECKING -from disnake import CommandInteraction, InviteTarget +from disnake import CommandInteraction from disnake.ext import commands 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 +from jukebot.utils import converter +from jukebot.views import PromoteView +from jukebot.views.embed import info_message + +if TYPE_CHECKING: + from jukebot import JukeBot class Utility(commands.Cog): @@ -28,7 +32,7 @@ async def info(self, inter: CommandInteraction): inter : CommandInteraction The interaction """ - e = embed.info_message() + e = info_message() e.add_field(name="🤖 Name", value=f"┕`{self.bot.user.display_name}`", inline=True) e.add_field(name="📡 Ping", value=f"┕`{self.bot.latency * 1000:.2f}ms`", inline=True) uptime = datetime.now() - self.bot.start_time @@ -64,32 +68,6 @@ async def links(self, inter: CommandInteraction): """ await inter.send(view=PromoteView()) - @commands.slash_command() - @commands.cooldown(1, 15.0, BucketType.guild) - @commands.max_concurrency(1, BucketType.guild) - @commands.check(checks.user_is_connected) - async def watch(self, inter: CommandInteraction): - """Launch a Youtube Together session in the voice channel where you are currently. - - Parameters - ---------- - inter : CommandInteraction - The interaction - """ - max_time = 180 - invite = await inter.author.voice.channel.create_invite( - max_age=max_time, - reason="Watch Together", - target_type=InviteTarget.embedded_application, - target_application=applications.default["youtube"], - ) - e = embed.activity_message( - "Watch Together started!", - f"An activity started in `{inter.author.voice.channel.name}`.\n", - ) - - await inter.send(embed=e, view=ActivityView(invite.url), delete_after=float(max_time)) - @commands.slash_command() @commands.cooldown(1, 15.0, BucketType.guild) async def reset(self, inter: CommandInteraction): @@ -102,7 +80,7 @@ async def reset(self, inter: CommandInteraction): """ await self.bot.services.reset(guild=inter.guild) - e = embed.info_message(content="The player has been reset.") + e = info_message(content="The player has been reset.") await inter.send(embed=e) diff --git a/jukebot/components/player.py b/jukebot/components/player.py index 6207ef2..277c677 100644 --- a/jukebot/components/player.py +++ b/jukebot/components/player.py @@ -2,11 +2,10 @@ import asyncio import os -from asyncio import Task -from enum import IntEnum, auto +from datetime import datetime +from enum import IntEnum, StrEnum, auto +from typing import TYPE_CHECKING -from disnake import CommandInteraction, VoiceChannel, VoiceClient -from disnake.ext.commands import Bot from loguru import logger from jukebot.components.audio_stream import AudioStream @@ -15,6 +14,14 @@ from jukebot.components.song import Song from jukebot.utils import coro +if TYPE_CHECKING: + from asyncio import Task + from datetime import timedelta + + from disnake import CommandInteraction, VoiceChannel, VoiceClient + + from jukebot.jukebot import JukeBot + class Player: class State(IntEnum): @@ -53,10 +60,10 @@ def is_inactive(self) -> bool: def is_leaving(self) -> bool: return self in [Player.State.STOPPED, Player.State.DISCONNECTING] - class Loop(IntEnum): - DISABLED = 0 - SONG = 1 - QUEUE = 2 + class Loop(StrEnum): + DISABLED = "none" + SONG = "song" + QUEUE = "queue" @property def is_song_loop(self) -> bool: @@ -66,8 +73,12 @@ def is_song_loop(self) -> bool: def is_queue_loop(self) -> bool: return self == Player.Loop.QUEUE - def __init__(self, bot: Bot, guild_id: int): - self.bot: Bot = bot + @classmethod + def values(cls) -> list[str]: + return [item.value for item in cls] + + def __init__(self, bot: JukeBot, guild_id: int): + self.bot: JukeBot = bot self._guild_id: int = guild_id self._voice: VoiceClient | None = None @@ -78,6 +89,7 @@ def __init__(self, bot: Bot, guild_id: int): self._state: Player.State = Player.State.IDLE self._idle_task: Task | None = None self._loop: Player.Loop = Player.Loop.DISABLED + self._session_start: datetime = datetime.now() async def join(self, channel: VoiceChannel): self._voice = await channel.connect(timeout=2.0) @@ -104,11 +116,13 @@ async def play(self, song: Song, replay: bool = False): self._song = song self.state = Player.State.PLAYING - async def disconnect(self, force=False): + async def disconnect(self, force=False) -> timedelta: if self._voice: self.state = Player.State.DISCONNECTING await self._voice.disconnect(force=force) + return datetime.now() - self._session_start + def skip(self): if self._voice: self.state = Player.State.SKIPPING @@ -144,7 +158,11 @@ def _after(self, error): return if self._loop.is_queue_loop: - func = self.bot.add_service(guild_id=self._guild_id, author=self.song.requester, query=self.song.web_url) + func = self.bot.services.add( + 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 diff --git a/jukebot/components/playerset.py b/jukebot/components/playerset.py index e621e26..f2112d2 100644 --- a/jukebot/components/playerset.py +++ b/jukebot/components/playerset.py @@ -1,22 +1,28 @@ from __future__ import annotations +from typing import TYPE_CHECKING, ClassVar + from jukebot.abstract_components import AbstractMap from jukebot.components.player import Player +if TYPE_CHECKING: + from jukebot import JukeBot + class PlayerSet(AbstractMap[int, Player]): + bot: ClassVar[JukeBot] _instance = None - def __new__(cls, bot): + def __new__(cls, bot: JukeBot): if cls._instance is None: cls._instance = super().__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 __getitem__(self, k: int) -> Player: + if not k in self._collection: + self._collection[k] = Player(self.bot, guild_id=k) + return self._collection[k] def playing(self) -> list[Player]: return [p for p in self._collection.values() if p.is_playing] diff --git a/jukebot/components/result.py b/jukebot/components/result.py index e2c0830..35bc41c 100644 --- a/jukebot/components/result.py +++ b/jukebot/components/result.py @@ -1,11 +1,13 @@ from __future__ import annotations from dataclasses import dataclass - -from disnake import Member +from typing import TYPE_CHECKING from jukebot.utils import converter +if TYPE_CHECKING: + from disnake import Member + @dataclass class Result: diff --git a/jukebot/components/resultset.py b/jukebot/components/resultset.py index 0c2eeb1..8456d27 100644 --- a/jukebot/components/resultset.py +++ b/jukebot/components/resultset.py @@ -2,12 +2,14 @@ import random from dataclasses import dataclass - -from disnake import Member +from typing import TYPE_CHECKING from jukebot.abstract_components import AbstractCollection from jukebot.components.result import Result +if TYPE_CHECKING: + from disnake import Member + @dataclass class ResultSet(AbstractCollection[Result]): @@ -22,7 +24,7 @@ def from_result(cls, results: list, requester: Member | None = None) -> ResultSe return cls(set=result_set) @classmethod - def empty(cls): + def empty(cls) -> ResultSet: return cls(set=[]) def get(self) -> Result: diff --git a/jukebot/components/song.py b/jukebot/components/song.py index 00b051b..d229ed2 100644 --- a/jukebot/components/song.py +++ b/jukebot/components/song.py @@ -1,11 +1,13 @@ from __future__ import annotations from dataclasses import dataclass - -from disnake import Member +from typing import TYPE_CHECKING from jukebot.utils import converter +if TYPE_CHECKING: + from disnake import Member + @dataclass class Song: diff --git a/jukebot/jukebot.py b/jukebot/jukebot.py index 193138d..cb259aa 100644 --- a/jukebot/jukebot.py +++ b/jukebot/jukebot.py @@ -3,6 +3,7 @@ import traceback from datetime import datetime from functools import cached_property +from typing import TYPE_CHECKING from disnake.ext import commands from loguru import logger @@ -12,9 +13,42 @@ from jukebot.components import PlayerSet from jukebot.utils import regex +if TYPE_CHECKING: + from jukebot.services import ResetService + from jukebot.services.music import ( + CurrentSongService, + GrabService, + JoinService, + LeaveService, + LoopService, + PauseService, + PlayService, + ResumeService, + SkipService, + StopService, + ) + from jukebot.services.queue import AddService, ClearService, RemoveService, ShowService, ShuffleService + class ServiceMap(AbstractMap[str, AbstractService]): - pass + """Typed service attributes, populated by JukeBot.add_service during cog setup.""" + + add: AddService + clear: ClearService + current_song: CurrentSongService + grab: GrabService + join: JoinService + leave: LeaveService + loop: LoopService + pause: PauseService + play: PlayService + remove: RemoveService + reset: ResetService + resume: ResumeService + show: ShowService + shuffle: ShuffleService + skip: SkipService + stop: StopService class JukeBot(commands.InteractionBot): @@ -35,6 +69,7 @@ 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`. + Declare its attribute in ServiceMap as well to enable static type checking and completion. Parameters ---------- diff --git a/jukebot/listeners/error_handler.py b/jukebot/listeners/error_handler.py index 97fa322..c8bf05b 100644 --- a/jukebot/listeners/error_handler.py +++ b/jukebot/listeners/error_handler.py @@ -1,12 +1,16 @@ from __future__ import annotations -from disnake import CommandInteraction +from typing import TYPE_CHECKING + from disnake.ext import commands -from disnake.ext.commands import CommandError from loguru import logger from jukebot import exceptions -from jukebot.utils import embed +from jukebot.views.embed import error_message, music_not_found_message + +if TYPE_CHECKING: + from disnake import CommandInteraction + from disnake.ext.commands import CommandError class ErrorHandler(commands.Cog): @@ -22,7 +26,7 @@ async def on_slash_command_error(self, inter: CommandInteraction, error: Command logger.opt(lazy=True).warning( f"Query Exception [{error.__class__.__name__}] '{error.query}' ({error.full_query}) for guild '{inter.guild.name} (ID: {inter.guild.id})'." ) - e = embed.music_not_found_message( + e = music_not_found_message( title=error, ) if inter.response.is_done(): @@ -31,7 +35,7 @@ async def on_slash_command_error(self, inter: CommandInteraction, error: Command await inter.send(embed=e, ephemeral=True) return - e = embed.error_message(content=error) + e = error_message(content=error) if inter.response.is_done(): await inter.edit_original_message(embed=e) else: diff --git a/jukebot/listeners/logger_handler.py b/jukebot/listeners/logger_handler.py index 1acf9d8..1b6ee30 100644 --- a/jukebot/listeners/logger_handler.py +++ b/jukebot/listeners/logger_handler.py @@ -1,14 +1,17 @@ from __future__ import annotations import traceback +from typing import TYPE_CHECKING -from disnake import CommandInteraction from disnake.ext import commands -from disnake.ext.commands import CommandError from loguru import logger from jukebot import exceptions +if TYPE_CHECKING: + from disnake import CommandInteraction + from disnake.ext.commands import CommandError + def fancy_traceback(exc: Exception) -> str: return "".join(traceback.format_exception(type(exc), exc, exc.__traceback__)) diff --git a/jukebot/listeners/voice_handler.py b/jukebot/listeners/voice_handler.py index cc9883f..ffbd0c6 100644 --- a/jukebot/listeners/voice_handler.py +++ b/jukebot/listeners/voice_handler.py @@ -2,11 +2,12 @@ from typing import TYPE_CHECKING -from disnake import Member, VoiceChannel, VoiceState from disnake.ext import commands -from disnake.ext.commands import Bot if TYPE_CHECKING: + from disnake import Member, VoiceChannel, VoiceState + from disnake.ext.commands import Bot + from jukebot.components import Player diff --git a/jukebot/services/music/current_song_service.py b/jukebot/services/music/current_song_service.py index a7a128e..d5a325a 100644 --- a/jukebot/services/music/current_song_service.py +++ b/jukebot/services/music/current_song_service.py @@ -12,8 +12,8 @@ class CurrentSongService(AbstractService): async def __call__(self, /, guild_id: int): player: Player = self.bot.players[guild_id] - stream: AudioStream = player.stream - song: Song = player.song + stream: AudioStream | None = player.stream + song: Song | None = player.song loop: Player.Loop = player.loop return song, stream, loop diff --git a/jukebot/services/music/join_service.py b/jukebot/services/music/join_service.py index 874367f..1fc631a 100644 --- a/jukebot/services/music/join_service.py +++ b/jukebot/services/music/join_service.py @@ -3,12 +3,12 @@ import copy from typing import TYPE_CHECKING -from disnake import CommandInteraction - from jukebot.abstract_components import AbstractService from jukebot.exceptions import PlayerConnexionException if TYPE_CHECKING: + from disnake import CommandInteraction + from jukebot.components import Player diff --git a/jukebot/services/music/leave_service.py b/jukebot/services/music/leave_service.py index df1e887..df0c71d 100644 --- a/jukebot/services/music/leave_service.py +++ b/jukebot/services/music/leave_service.py @@ -6,4 +6,4 @@ class LeaveService(AbstractService): async def __call__(self, /, guild_id: int): # once the bot leave, we destroy is instance from the container - await self.bot.players.pop(guild_id).disconnect() + return 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 b0eba3c..583f903 100644 --- a/jukebot/services/music/loop_service.py +++ b/jukebot/services/music/loop_service.py @@ -1,26 +1,15 @@ from __future__ import annotations -from typing import TYPE_CHECKING - -from jukebot import components from jukebot.abstract_components import AbstractService - -if TYPE_CHECKING: - from jukebot.components import Player +from jukebot.components import Player class LoopService(AbstractService): 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" - elif mode == "queue": - player.loop = components.Player.Loop.QUEUE - new_status = "Loop is set to queue" - elif mode == "none": - player.loop = components.Player.Loop.DISABLED - new_status = "Loop is disabled" - return new_status + old_status = player.loop + new_status = Player.Loop(mode) + player.loop = new_status + + return old_status, new_status diff --git a/jukebot/services/music/play_service.py b/jukebot/services/music/play_service.py index 4cfcd33..4ddcfa5 100644 --- a/jukebot/services/music/play_service.py +++ b/jukebot/services/music/play_service.py @@ -2,7 +2,6 @@ from typing import TYPE_CHECKING -from disnake import CommandInteraction from loguru import logger from jukebot import components @@ -11,6 +10,8 @@ from jukebot.exceptions.player_exception import PlayerConnexionException if TYPE_CHECKING: + from disnake import CommandInteraction + from jukebot.components import Player, Result, Song diff --git a/jukebot/services/queue/add_service.py b/jukebot/services/queue/add_service.py index 8954ca2..82405a5 100644 --- a/jukebot/services/queue/add_service.py +++ b/jukebot/services/queue/add_service.py @@ -2,14 +2,14 @@ from typing import TYPE_CHECKING -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 if TYPE_CHECKING: + from disnake import Member + from jukebot.components import Player, Result, ResultSet diff --git a/jukebot/services/reset_service.py b/jukebot/services/reset_service.py index 7172d39..91e9731 100644 --- a/jukebot/services/reset_service.py +++ b/jukebot/services/reset_service.py @@ -1,12 +1,15 @@ +from __future__ import annotations + from typing import TYPE_CHECKING -from disnake import Guild from loguru import logger from jukebot.abstract_components import AbstractService from jukebot.exceptions.player_exception import PlayerDontExistException if TYPE_CHECKING: + from disnake import Guild + from jukebot.components import Player diff --git a/jukebot/utils/aioweb.py b/jukebot/utils/aioweb.py index 00bce32..24ad0bc 100644 --- a/jukebot/utils/aioweb.py +++ b/jukebot/utils/aioweb.py @@ -10,17 +10,17 @@ @alib.lru_cache(maxsize=1024) -async def cached_query(url, enquote_url: bool = False) -> (int, str): +async def cached_query(url, enquote_url: bool = False) -> tuple[int, str]: logger.opt(lazy=True).info(f"Cached query for url {url}") return await _get(url, enquote_url) -async def uncached_query(url, enquote_url: bool = False) -> (int, str): +async def uncached_query(url, enquote_url: bool = False) -> tuple[int, str]: logger.opt(lazy=True).info(f"Uncached query for url {url}") return await _get(url, enquote_url) -async def _get(url, enquote) -> (int, str): +async def _get(url, enquote) -> tuple[int, str]: url = url if not enquote else parse.quote(url) async with aiohttp.ClientSession(headers=_MOZ_HEADER) as session, session.get(url) as rep: diff --git a/jukebot/utils/applications.py b/jukebot/utils/applications.py deleted file mode 100644 index fb0465c..0000000 --- a/jukebot/utils/applications.py +++ /dev/null @@ -1,17 +0,0 @@ -# credit to discord-together lib -# https://github.com/apurv-r/discord-together/blob/main/discord_together/discordTogetherMain.py#L6-L22 -default = { - # Credits to RemyK888 - "youtube": "880218394199220334", - "poker": "755827207812677713", - "betrayal": "773336526917861400", - "fishing": "814288819477020702", - "chess": "832012774040141894", - # Credits to awesomehet2124 - "letter-tile": "879863686565621790", - "word-snack": "879863976006127627", - "doodle-crew": "878067389634314250", - "spellcast": "852509694341283871", - "awkword": "879863881349087252", - "checkers": "832013003968348200", -} diff --git a/jukebot/utils/checks.py b/jukebot/utils/checks.py index f960e08..962f949 100644 --- a/jukebot/utils/checks.py +++ b/jukebot/utils/checks.py @@ -2,10 +2,11 @@ from typing import TYPE_CHECKING -from disnake import CommandInteraction, VoiceClient from disnake.ext.commands import CheckFailure if TYPE_CHECKING: + from disnake import CommandInteraction, VoiceClient + from jukebot.components import Player diff --git a/jukebot/utils/converter.py b/jukebot/utils/converter.py index 83559e9..80c9bc2 100644 --- a/jukebot/utils/converter.py +++ b/jukebot/utils/converter.py @@ -2,17 +2,15 @@ import re -import yaml - -def seconds_to_time(seconds: int) -> (int, int, int, int): +def seconds_to_time(seconds: int) -> tuple[int, int, int, int]: h, r = divmod(seconds, 3600) m, s = divmod(r, 60) d, h = divmod(h, 24) return d, h, m, s -def time_to_youtube_format(time: (int, int, int, int)) -> str: +def time_to_youtube_format(time: tuple[int, int, int, int]) -> str: return re.sub(r"^[0:]*(.+:..)$", r"\1", ":".join([f"{e:02d}" for e in time])) @@ -40,12 +38,3 @@ def duration_seconds_to_progress_bar(time: int, total: int, ticks: int = 30) -> x = int(ticks * (time / total)) if total else 0 line = "".join(["▬" if t != x else "🔘" for t in range(ticks)]) return line - - -def radios_yaml_to_dict() -> dict: - radios: dict = {} - with open("./data/radios.yaml", "r") as f: - data = yaml.safe_load(f) - for e in data: - radios.update(e) - return radios diff --git a/jukebot/utils/embed.py b/jukebot/utils/embed.py deleted file mode 100644 index 2950387..0000000 --- a/jukebot/utils/embed.py +++ /dev/null @@ -1,213 +0,0 @@ -from __future__ import annotations - -import itertools -import random -from typing import TYPE_CHECKING - -import disnake -from disnake import APISlashCommand, Member -from disnake.ext.commands import Bot - -from jukebot.utils import converter - -if TYPE_CHECKING: - from jukebot.components import Result, ResultSet, Song - from jukebot.components.player import Player - -VOID_TOKEN = "\u200b" - - -def _base_embed(content="", color=0x38383D): - return disnake.Embed(title="", description=content, color=color) - - -def error_message(title="", content=""): - embed: disnake.Embed = _base_embed(content=content, color=0xDB3C30) - embed.set_author( - name="Error" if title == "" else title, - icon_url="https://icons.iconarchive.com/icons/papirus-team/papirus-status/512/dialog-error-icon.png", - ) - return embed - - -def info_message(title="", content=""): - embed: disnake.Embed = _base_embed(content=content, color=0x30A3DB) - - embed.set_author( - name="Information" if title == "" else title, - icon_url="https://icons.iconarchive.com/icons/papirus-team/papirus-status/512/dialog-information-icon.png", - ) - return embed - - -def basic_message(title="", content=""): - embed: disnake.Embed = _base_embed(content=content, color=0x4F4F4F) - embed.set_author( - name="Information" if title == "" else title, - icon_url="https://cdn.discordapp.com/attachments/573225654452092930/908327963718713404/juke-icon.png", - ) - return embed - - -def activity_message(title="", content=""): - colors = [0xF6C333, 0xF4B400] - c = colors[random.randint(0, 1)] - 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/dragon-ball-online-global-icon.png", - ) - return embed - - -def music_message(song: Song, loop_mode: Player.Loop, current_duration: int = 0): - colors = [0x736DAB, 0xFFBA58] - c = colors[random.randint(0, 1)] - - embed: disnake.Embed = _base_embed(content="", color=c) - embed.set_author( - name=song.title, - url=song.web_url, - icon_url="https://icons.iconarchive.com/icons/papirus-team/papirus-apps/512/musicbrainz-icon.png", - ) - - if song.thumbnail: - embed.set_thumbnail(url=song.thumbnail) - - embed.add_field(name="Channel", value=song.channel, inline=True) - if current_duration: - embed.add_field(VOID_TOKEN, VOID_TOKEN, inline=True) - embed.add_field("Loop", loop_mode.name.capitalize(), inline=True) - - if current_duration: - line = converter.duration_seconds_to_progress_bar(current_duration, song.duration) - fmt_current: str = converter.seconds_to_youtube_format(current_duration) - embed.add_field(name="Progression", value=f"`{fmt_current} {line} {song.fmt_duration}`") - else: - embed.add_field(name="Duration", value=f"`{song.fmt_duration}`") - - return embed - - -def music_search_message(title="", content=""): - embed: disnake.Embed = _base_embed(content=content, color=0x4F4F4F) - embed.set_author( - name="Search" if title == "" else title, - icon_url="https://icons.iconarchive.com/icons/papirus-team/papirus-apps/512/d-feet-icon.png", - ) - return embed - - -def music_not_found_message(title="", content=""): - embed: disnake.Embed = _base_embed(content=content, color=0xEBA229) - embed.set_author( - name="Error" if title == "" else title, - icon_url="https://icons.iconarchive.com/icons/papirus-team/papirus-apps/512/plasma-search-icon.png", - ) - return embed - - -def music_found_message(music: dict, title=""): - links = " | ".join(f"[{name}]({url})" for name, url in music["links"].items()) - embed: disnake.Embed = _base_embed(color=0x54B23F) - embed.set_author( - name="Music found!" if title == "" else title, - icon_url="https://cdn.discordapp.com/attachments/573225654452092930/952197615221612594/d-feet-icon.png", - ) - title = f"[{music['title']}]({music['url']})" if music["url"] else music["title"] - embed.add_field(name="Title", value=title, inline=False) - embed.add_field(name="Artist", value=music["author"], inline=True) - embed.add_field(name=VOID_TOKEN, value=VOID_TOKEN, inline=True) - embed.add_field(name="Links", value=links, inline=True) - if isinstance(music["image_url"], str) and music["image_url"].startswith(("http://", "https://")): - embed.set_thumbnail(url=music["image_url"]) - return embed - - -def search_result_message(playlist: ResultSet, title=""): - content = "\n\n".join( - [ - f"{converter.number_to_emoji(i)} `{s.title} by {s.channel}` **[{s.fmt_duration}]**" - for i, s in enumerate(playlist, start=1) - ] - ) - embed: disnake.Embed = music_search_message(title=title, content=content) - embed.add_field( - name=VOID_TOKEN, - value="Use the selector below to choose a result.", - ) - return embed - - -def basic_queue_message(title="", content=""): - colors = [0x438F96, 0x469961, 0x3F3F3F] - c = colors[random.randint(0, 2)] - embed: disnake.Embed = _base_embed(content=content, color=c) - embed.set_author( - name="Information" if title == "" else title, - icon_url="https://cdn.icon-icons.com/icons2/1381/PNG/512/xt7playermpv_94294.png", - ) - return embed - - -def queue_message(playlist: ResultSet, bot: Bot, title=""): - playlist_slice = itertools.islice(playlist, 10) - content = "\n\n".join( - [ - f"`{i}` • `{s.title}` on `{s.channel}` **[{s.fmt_duration}]** — `{s.requester}`" - for i, s in enumerate(playlist_slice, start=1) - ] - ) - embed: disnake.Embed = basic_queue_message(title=title, content=content) - embed.add_field(name="Total songs", value=f"`{len(playlist)}`") - total_time: int = sum([e.duration for e in playlist if not e.live]) - total_time_fmt: str = converter.seconds_to_youtube_format(total_time) - embed.add_field(name="Total duration", value=f"`{total_time_fmt}`") - - cmd: APISlashCommand = bot.get_global_command_named("queue") - embed.add_field( - name=VOID_TOKEN, - value=f"Use command or to add or remove a song.", - inline=False, - ) - return embed - - -def result_enqueued(res: Result): - colors = [0x438F96, 0x469961, 0x3F3F3F] - c = colors[random.randint(0, 2)] - embed: disnake.Embed = _base_embed(content="", color=c) - embed.set_author( - name=f"Enqueued : {res.title}", - url=res.web_url, - 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) - return embed - - -def grab_message(song: Song, current_duration: int = 0): - embed: disnake.Embed = _base_embed(content=f"[{song.title}]({song.web_url})", color=0x366ADB) - embed.set_author( - name="Saved music", - icon_url="https://icons.iconarchive.com/icons/papirus-team/papirus-apps/512/atunes-icon.png", - ) - embed.add_field(name="Channel", value=song.channel) - fmt_current: str = converter.seconds_to_youtube_format(current_duration) - embed.add_field(name="Time code", value=f"`{fmt_current}/{song.fmt_duration}`") - if song.thumbnail: - embed.set_thumbnail(url=song.thumbnail) - return embed - - -def share_message(author: Member, content, title="", url="", img=""): - embed: disnake.Embed = _base_embed(content=content, color=0x366ADB) - embed.set_author( - name=title if title else f"Music shared by {author}", - icon_url="https://icons.iconarchive.com/icons/papirus-team/papirus-apps/512/atunes-icon.png", - url=url, - ) - if img: - embed.set_thumbnail(url=img) - return embed diff --git a/jukebot/views/__init__.py b/jukebot/views/__init__.py index 1442902..b72270b 100644 --- a/jukebot/views/__init__.py +++ b/jukebot/views/__init__.py @@ -1,4 +1,3 @@ -from .activity_view import ActivityView as ActivityView from .promote_view import PromoteView as PromoteView from .search_view import SearchDropdownView as SearchDropdownView from .search_view import SearchInteraction as SearchInteraction diff --git a/jukebot/views/activity_view.py b/jukebot/views/activity_view.py deleted file mode 100644 index 5839e9a..0000000 --- a/jukebot/views/activity_view.py +++ /dev/null @@ -1,15 +0,0 @@ -import disnake - - -class _JoinActivityButton(disnake.ui.Button): - def __init__(self, url): - label = "Join the activity" - emoji = "🌠" - - super().__init__(url=url, label=label, emoji=emoji) - - -class ActivityView(disnake.ui.View): - def __init__(self, code): - super().__init__() - self.add_item(_JoinActivityButton(code)) diff --git a/jukebot/views/container/__init__.py b/jukebot/views/container/__init__.py new file mode 100644 index 0000000..9e141a2 --- /dev/null +++ b/jukebot/views/container/__init__.py @@ -0,0 +1 @@ +from .basic import basic_message as basic_message diff --git a/jukebot/views/container/basic.py b/jukebot/views/container/basic.py new file mode 100644 index 0000000..19de23f --- /dev/null +++ b/jukebot/views/container/basic.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from disnake import ui + + +def basic_message(title: str, content: str | list[str] | None = None) -> ui.Container: + if content is None: + return ui.Container( + ui.TextDisplay(f"### {title}"), + ) + + content = content if isinstance(content, list) else [content] + txt_content = [ui.TextDisplay(f"-# {c}") if c != "" else ui.Separator() for c in content] + return ui.Container( + ui.TextDisplay(f"### {title}"), + ui.Separator(), + *txt_content, + ) diff --git a/jukebot/views/embed/__init__.py b/jukebot/views/embed/__init__.py new file mode 100644 index 0000000..68eef10 --- /dev/null +++ b/jukebot/views/embed/__init__.py @@ -0,0 +1,14 @@ +from .base import VOID_TOKEN as VOID_TOKEN +from .general import basic_message as basic_message +from .general import error_message as error_message +from .general import info_message as info_message +from .music import music_message as music_message +from .queue import basic_queue_message as basic_queue_message +from .queue import queue_message as queue_message +from .queue import result_enqueued as result_enqueued +from .search import music_found_message as music_found_message +from .search import music_not_found_message as music_not_found_message +from .search import music_search_message as music_search_message +from .search import search_result_message as search_result_message +from .share import grab_message as grab_message +from .share import share_message as share_message diff --git a/jukebot/views/embed/base.py b/jukebot/views/embed/base.py new file mode 100644 index 0000000..099ff60 --- /dev/null +++ b/jukebot/views/embed/base.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +from disnake import Embed + +VOID_TOKEN = "\u200b" + + +def _base_embed(content="", color=0x38383D) -> Embed: + return Embed(title="", description=content, color=color) diff --git a/jukebot/views/embed/general.py b/jukebot/views/embed/general.py new file mode 100644 index 0000000..17afdf8 --- /dev/null +++ b/jukebot/views/embed/general.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .base import _base_embed + +if TYPE_CHECKING: + from disnake import Embed + + +def basic_message(title="", content=""): + embed: Embed = _base_embed(content=content, color=0x4F4F4F) + embed.set_author( + name="Information" if title == "" else title, + icon_url="https://cdn.discordapp.com/attachments/573225654452092930/908327963718713404/juke-icon.png", + ) + return embed + + +def info_message(title="", content=""): + embed: Embed = _base_embed(content=content, color=0x30A3DB) + + embed.set_author( + name="Information" if title == "" else title, + icon_url="https://icons.iconarchive.com/icons/papirus-team/papirus-status/512/dialog-information-icon.png", + ) + return embed + + +def error_message(title="", content=""): + embed: Embed = _base_embed(content=content, color=0xDB3C30) + embed.set_author( + name="Error" if title == "" else title, + icon_url="https://icons.iconarchive.com/icons/papirus-team/papirus-status/512/dialog-error-icon.png", + ) + return embed diff --git a/jukebot/views/embed/music.py b/jukebot/views/embed/music.py new file mode 100644 index 0000000..cecb89f --- /dev/null +++ b/jukebot/views/embed/music.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import random +from typing import TYPE_CHECKING + +from jukebot.utils import converter + +from .base import VOID_TOKEN, _base_embed + +if TYPE_CHECKING: + from disnake import Embed + + from jukebot.components import Song + from jukebot.components.player import Player + + +def music_message(song: Song, loop_mode: Player.Loop, current_duration: int = 0): + colors = [0x736DAB, 0xFFBA58] + c = random.choice(colors) + + embed: Embed = _base_embed(content="", color=c) + embed.set_author( + name=song.title, + url=song.web_url, + icon_url="https://icons.iconarchive.com/icons/papirus-team/papirus-apps/512/musicbrainz-icon.png", + ) + + if song.thumbnail: + embed.set_thumbnail(url=song.thumbnail) + + embed.add_field(name="Channel", value=song.channel, inline=True) + if current_duration: + embed.add_field(VOID_TOKEN, VOID_TOKEN, inline=True) + embed.add_field("Loop", loop_mode.name.capitalize(), inline=True) + + if current_duration: + line = converter.duration_seconds_to_progress_bar(current_duration, song.duration) + fmt_current: str = converter.seconds_to_youtube_format(current_duration) + embed.add_field(name="Progression", value=f"`{fmt_current} {line} {song.fmt_duration}`") + else: + embed.add_field(name="Duration", value=f"`{song.fmt_duration}`") + + return embed diff --git a/jukebot/views/embed/queue.py b/jukebot/views/embed/queue.py new file mode 100644 index 0000000..9f38a55 --- /dev/null +++ b/jukebot/views/embed/queue.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +import itertools +import random +from typing import TYPE_CHECKING + +from jukebot.utils import converter + +from .base import _base_embed + +if TYPE_CHECKING: + from disnake import Embed + + from jukebot.components import Result, ResultSet + + +def basic_queue_message(title="", content=""): + colors = [0x438F96, 0x469961, 0x3F3F3F] + c = random.choice(colors) + embed: Embed = _base_embed(content=content, color=c) + embed.set_author( + name="Information" if title == "" else title, + icon_url="https://cdn.icon-icons.com/icons2/1381/PNG/512/xt7playermpv_94294.png", + ) + return embed + + +def queue_message(playlist: ResultSet, title=""): + playlist_slice = itertools.islice(playlist, 10) + content = "\n\n".join( + [ + f"`{i}` • `{s.title}` on `{s.channel}` **[{s.fmt_duration}]** — `{s.requester}`" + for i, s in enumerate(playlist_slice, start=1) + ] + ) + embed: Embed = basic_queue_message(title=title, content=content) + embed.add_field(name="Total songs", value=f"`{len(playlist)}`") + total_time: int = sum([e.duration for e in playlist if not e.live]) + total_time_fmt: str = converter.seconds_to_youtube_format(total_time) + embed.add_field(name="Total duration", value=f"`{total_time_fmt}`") + + return embed + + +def result_enqueued(res: Result): + colors = [0x438F96, 0x469961, 0xF3F3F] + c = random.choice(colors) + embed: Embed = _base_embed(content="", color=c) + embed.set_author( + name=f"Enqueued : {res.title}", + url=res.web_url, + 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) + return embed diff --git a/jukebot/views/embed/search.py b/jukebot/views/embed/search.py new file mode 100644 index 0000000..9ad900d --- /dev/null +++ b/jukebot/views/embed/search.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from jukebot.utils import converter + +from .base import VOID_TOKEN, _base_embed + +if TYPE_CHECKING: + from disnake import Embed + + from jukebot.components import ResultSet + + +def music_search_message(title="", content=""): + embed: Embed = _base_embed(content=content, color=0x4F4F4F) + embed.set_author( + name="Search" if title == "" else title, + icon_url="https://icons.iconarchive.com/icons/papirus-team/papirus-apps/512/d-feet-icon.png", + ) + return embed + + +def search_result_message(playlist: ResultSet, title=""): + content = "\n\n".join( + [ + f"{converter.number_to_emoji(i)} `{s.title} by {s.channel}` **[{s.fmt_duration}]**" + for i, s in enumerate(playlist, start=1) + ] + ) + embed: Embed = music_search_message(title=title, content=content) + embed.add_field( + name=VOID_TOKEN, + value="Use the selector below to choose a result.", + ) + return embed + + +def music_found_message(music: dict, title=""): + links = " | ".join(f"[{name}]({url})" for name, url in music["links"].items()) + embed: Embed = _base_embed(color=0x54B23F) + embed.set_author( + name="Music found!" if title == "" else title, + icon_url="https://cdn.discordapp.com/attachments/573225654452092930/952197615221612594/d-feet-icon.png", + ) + title = f"[{music['title']}]({music['url']})" if music["url"] else music["title"] + embed.add_field(name="Title", value=title, inline=False) + embed.add_field(name="Artist", value=music["author"], inline=True) + embed.add_field(name=VOID_TOKEN, value=VOID_TOKEN, inline=True) + embed.add_field(name="Links", value=links, inline=True) + if isinstance(music["image_url"], str) and music["image_url"].startswith(("http://", "https://")): + embed.set_thumbnail(url=music["image_url"]) + return embed + + +def music_not_found_message(title="", content=""): + embed: Embed = _base_embed(content=content, color=0xEBA229) + embed.set_author( + name="Error" if title == "" else title, + icon_url="https://icons.iconarchive.com/icons/papirus-team/papirus-apps/512/plasma-search-icon.png", + ) + return embed diff --git a/jukebot/views/embed/share.py b/jukebot/views/embed/share.py new file mode 100644 index 0000000..1a0874d --- /dev/null +++ b/jukebot/views/embed/share.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from jukebot.utils import converter + +from .base import _base_embed + +if TYPE_CHECKING: + from disnake import Embed, Member + + from jukebot.components import Song + + +def grab_message(song: Song, current_duration: int = 0): + embed: Embed = _base_embed(content=f"[{song.title}]({song.web_url})", color=0x366ADB) + embed.set_author( + name="Saved music", + icon_url="https://icons.iconarchive.com/icons/papirus-team/papirus-apps/512/atunes-icon.png", + ) + embed.add_field(name="Channel", value=song.channel) + fmt_current: str = converter.seconds_to_youtube_format(current_duration) + embed.add_field(name="Time code", value=f"`{fmt_current}/{song.fmt_duration}`") + if song.thumbnail: + embed.set_thumbnail(url=song.thumbnail) + return embed + + +def share_message(author: Member, content, title="", url="", img=""): + embed: Embed = _base_embed(content=content, color=0x366ADB) + embed.set_author( + name=title if title else f"Music shared by {author}", + icon_url="https://icons.iconarchive.com/icons/papirus-team/papirus-apps/512/atunes-icon.png", + url=url, + ) + if img: + embed.set_thumbnail(url=img) + return embed diff --git a/jukebot/views/promote_view.py b/jukebot/views/promote_view.py index 345af1f..44a4a54 100644 --- a/jukebot/views/promote_view.py +++ b/jukebot/views/promote_view.py @@ -1,9 +1,9 @@ import os -import disnake +from disnake import ui -class _VoteButton(disnake.ui.Button): +class _VoteButton(ui.Button): def __init__(self): url = os.environ["BOT_VOTE_URL"] label = "Vote" @@ -12,7 +12,7 @@ def __init__(self): super().__init__(url=url, label=label, emoji=emoji) -class _InviteButton(disnake.ui.Button): +class _InviteButton(ui.Button): def __init__(self): url = os.environ["BOT_INVITE_URL"] label = "Invite me" @@ -21,7 +21,7 @@ def __init__(self): super().__init__(url=url, label=label, emoji=emoji) -class _DonateButton(disnake.ui.Button): +class _DonateButton(ui.Button): def __init__(self): url = os.environ["BOT_DONATE_URL"] label = "Donate" @@ -30,7 +30,7 @@ def __init__(self): super().__init__(url=url, label=label, emoji=emoji) -class _ServerButton(disnake.ui.Button): +class _ServerButton(ui.Button): def __init__(self): url = os.environ["BOT_SERVER_URL"] label = "Community" @@ -39,7 +39,7 @@ def __init__(self): super().__init__(url=url, label=label, emoji=emoji) -class PromoteView(disnake.ui.View): +class PromoteView(ui.View): def __init__(self): super().__init__() self.add_item(_InviteButton()) diff --git a/jukebot/views/search_view.py b/jukebot/views/search_view.py index 7e24bff..d9b4e74 100644 --- a/jukebot/views/search_view.py +++ b/jukebot/views/search_view.py @@ -3,10 +3,11 @@ import os from typing import TYPE_CHECKING -import disnake -from disnake import Interaction, Member +from disnake import SelectOption, ui if TYPE_CHECKING: + from disnake import Interaction, Member + from jukebot.components import ResultSet @@ -27,11 +28,11 @@ class SearchInteraction: ) -class _SearchDropdown(disnake.ui.Select): +class _SearchDropdown(ui.Select): def __init__(self, results: ResultSet): self._results = results options = [ - disnake.SelectOption( + SelectOption( label=r.title, value=r.web_url, description=f"on {r.channel} — {r.fmt_duration}", @@ -40,7 +41,7 @@ def __init__(self, results: ResultSet): for i, r in enumerate(results) ] options.append( - disnake.SelectOption( + SelectOption( label="Cancel", value=SearchInteraction.CANCEL_TEXT, description="Cancel the current search", @@ -56,7 +57,7 @@ def __init__(self, results: ResultSet): ) -class SearchDropdownView(disnake.ui.View): +class SearchDropdownView(ui.View): def __init__(self, author: Member, results: ResultSet): super().__init__(timeout=float(os.environ["BOT_SEARCH_TIMEOUT"])) self._author = author diff --git a/tests/test_abstract_components.py b/tests/test_abstract_components.py new file mode 100644 index 0000000..0c82d23 --- /dev/null +++ b/tests/test_abstract_components.py @@ -0,0 +1,184 @@ +import unittest +from unittest.mock import AsyncMock, Mock + +from jukebot.abstract_components import AbstractCollection, AbstractMap, AbstractRequest, AbstractService + + +class AbstractCollectionTest(unittest.TestCase): + def test_empty_collection(self): + collection = AbstractCollection(set=[]) + self.assertEqual(len(collection), 0) + self.assertEqual(list(collection), []) + self.assertNotIn("missing", collection) + with self.assertRaises(IndexError): + collection[0] + + def test_order_duplicates_and_indexing(self): + collection = AbstractCollection(set=[3, 1, 3]) + self.assertEqual(len(collection), 3) + self.assertEqual(list(collection), [3, 1, 3]) + self.assertEqual(collection[1], 1) + self.assertEqual(collection[-1], 3) + self.assertIn(3, collection) + self.assertNotIn("3", collection) + self.assertEqual(str(collection), "[3, 1, 3]") + with self.assertRaises(IndexError): + collection[3] + + def test_add_extends_left_collection_without_changing_right(self): + left = AbstractCollection(set=[1, 2]) + right = AbstractCollection(set=[2, 3]) + self.assertIs(left + right, left) + self.assertEqual(list(left), [1, 2, 2, 3]) + self.assertEqual(list(right), [2, 3]) + + +class AbstractMapTest(unittest.TestCase): + def test_mapping_operations(self): + mapping = AbstractMap[str, int]() + self.assertEqual(len(mapping), 0) + mapping["first"] = 1 + mapping["second"] = 2 + mapping["first"] = 3 + self.assertEqual(mapping["first"], 3) + self.assertEqual(len(mapping), 2) + self.assertEqual(list(mapping), ["first", "second"]) + self.assertEqual(dict(mapping), {"first": 3, "second": 2}) + self.assertEqual(str(mapping), "{'first': 3, 'second': 2}") + self.assertIn("first", mapping) + self.assertNotIn(42, mapping) + del mapping["first"] + self.assertNotIn("first", mapping) + + def test_missing_keys(self): + mapping = AbstractMap[str, int]() + with self.assertRaises(KeyError): + mapping["missing"] + with self.assertRaises(KeyError): + del mapping["missing"] + self.assertIsNone(mapping.get("missing")) + self.assertEqual(mapping.get("missing", 7), 7) + self.assertEqual(len(mapping), 0) + + def test_inherited_mapping_methods(self): + mapping = AbstractMap[str, int]() + mapping.update({"first": 1, "second": 2}) + self.assertEqual(list(mapping.values()), [1, 2]) + self.assertEqual(list(mapping.items()), [("first", 1), ("second", 2)]) + self.assertEqual(mapping.setdefault("first", 99), 1) + self.assertEqual(mapping.pop("second"), 2) + mapping.clear() + self.assertEqual(dict(mapping), {}) + + def test_instances_are_independent_even_without_super_init(self): + class NamedMap(AbstractMap[str, int]): + def __init__(self, name): + self.name = name + + first = NamedMap("first") + second = NamedMap(name="second") + self.assertEqual(first.name, "first") + self.assertEqual(second.name, "second") + first["key"] = 1 + self.assertNotIn("key", second) + second["key"] = 2 + first.clear() + self.assertEqual(second["key"], 2) + + def test_contains_does_not_trigger_lazy_getitem(self): + class LazyMap(AbstractMap[str, int]): + def __getitem__(self, k): + if k not in self._collection: + self._collection[k] = 0 + return self._collection[k] + + mapping = LazyMap() + self.assertNotIn("missing", mapping) + self.assertEqual(len(mapping), 0) + self.assertEqual(mapping["missing"], 0) + self.assertIn("missing", mapping) + + +class StubRequest(AbstractRequest): + async def setup(self): + pass + + async def execute(self): + self._result = "result" + self._success = True + + async def terminate(self): + pass + + +class AbstractRequestTest(unittest.IsolatedAsyncioTestCase): + def test_base_class_cannot_be_instantiated(self): + with self.assertRaises(TypeError): + AbstractRequest("query") + + async def test_lifecycle_and_public_state(self): + request = StubRequest("query") + request.setup = AsyncMock() + request.terminate = AsyncMock() + self.assertEqual(request.query, "query") + self.assertIsNone(request.result) + self.assertFalse(request.success) + + async with request as entered: + self.assertIs(entered, request) + request.setup.assert_awaited_once_with() + request.terminate.assert_not_awaited() + self.assertFalse(request.success) + await request.execute() + self.assertTrue(request.success) + self.assertEqual(request.result, "result") + + request.terminate.assert_awaited_once_with() + + async def test_execution_failure_cleans_up_and_propagates(self): + request = StubRequest("query") + error = RuntimeError("execution failed") + request.execute = AsyncMock(side_effect=error) + request.terminate = AsyncMock() + with self.assertRaises(RuntimeError) as caught: + async with request: + await request.execute() + self.assertIs(caught.exception, error) + request.terminate.assert_awaited_once_with() + self.assertFalse(request.success) + + async def test_setup_failure_does_not_enter_context(self): + request = StubRequest("query") + request.setup = AsyncMock(side_effect=RuntimeError("setup failed")) + request.terminate = AsyncMock() + with self.assertRaisesRegex(RuntimeError, "setup failed"): + async with request: + self.fail("Context must not be entered after setup fails") + request.terminate.assert_not_awaited() + + async def test_cleanup_failure_propagates(self): + request = StubRequest("query") + request.terminate = AsyncMock(side_effect=RuntimeError("cleanup failed")) + with self.assertRaisesRegex(RuntimeError, "cleanup failed"): + async with request: + pass + + +class AbstractServiceTest(unittest.IsolatedAsyncioTestCase): + async def test_base_call_requires_implementation(self): + service = AbstractService(Mock()) + with self.assertRaises(NotImplementedError): + await service() + + def test_context_preserves_service_and_bot(self): + bot = Mock() + service = AbstractService(bot) + with service as entered: + self.assertIs(entered, service) + self.assertIs(entered.bot, bot) + + def test_context_does_not_suppress_errors(self): + error = RuntimeError("service failed") + with self.assertRaises(RuntimeError) as caught, AbstractService(Mock()): + raise error + self.assertIs(caught.exception, error) diff --git a/tests/test_music_request.py b/tests/test_music_request.py index e1d7531..f7d32cc 100644 --- a/tests/test_music_request.py +++ b/tests/test_music_request.py @@ -527,7 +527,7 @@ async def test_music_request_playlist_soundcloud_url_convert_to_resultset(self): async def test_music_request_success_soundcloud_shorted_url_convert_to_result(self): with disable_logging(): - async with MusicRequest("https://on.soundcloud.com/Gsdzc") as req: + async with MusicRequest("https://on.soundcloud.com/PfY6LKyoMb8FkogMdF") as req: await req.execute() self.assertTrue(req.success) @@ -536,18 +536,18 @@ async def test_music_request_success_soundcloud_shorted_url_convert_to_result(se result: Result = Result(req.result) - self.assertEqual(result.title, "Headband Andy Vito Bad Boy") - self.assertEqual(result.channel, "Minecraft Pukaj 009 Sk") + self.assertEqual(result.title, "Remember Tomorrow") + self.assertEqual(result.channel, "Deathbrainofficial") self.assertEqual( result.web_url, - "https://soundcloud.com/minecraft-pukaj-009-sk/headband-andy-vito-bad-boy", + "https://soundcloud.com/deathbrainofficial/remember-tomorrow", ) self.assertEqual(result.duration, 0) self.assertEqual(result.fmt_duration, "ထ") self.assertTrue(result.live) self.assertIsNone(result.requester) - @unittest.skipIf(os.getenv("CI"),("not working due to soundcloud private link update")) + @unittest.skipIf(os.getenv("CI"), ("not working due to soundcloud private link update")) async def test_music_request_success_soundcloud_shorted_private_url_convert_to_result(self): async with MusicRequest("https://on.soundcloud.com/gA4Ca") as req: await req.execute() @@ -558,11 +558,11 @@ async def test_music_request_success_soundcloud_shorted_private_url_convert_to_r result: Result = Result(req.result) - self.assertEqual(result.title, "Empty") - self.assertEqual(result.channel, "Dysta") + self.assertEqual(result.title, "The Prelude 001 185") + self.assertEqual(result.channel, "Kbt Thomas") self.assertEqual( result.web_url, - "https://soundcloud.com/dysta/empty/s-wEHdWGqgDdf", + "https://soundcloud.com/kbt-thomas/the-prelude-001-185/s-FkBDkSV4oOB", ) self.assertEqual(result.duration, 0) self.assertEqual(result.fmt_duration, "ထ") diff --git a/tests/test_radios.py b/tests/test_radios.py index e5c9998..a7bf3d6 100644 --- a/tests/test_radios.py +++ b/tests/test_radios.py @@ -1,17 +1,19 @@ import os import unittest +import yaml + from jukebot.components.requests import MusicRequest -from jukebot.utils import converter from jukebot.utils.logging import disable_logging class TestRadios(unittest.IsolatedAsyncioTestCase): @classmethod def setUpClass(cls): - cls._radios: dict = converter.radios_yaml_to_dict() + with open("./data/radios.yaml", "r") as f: + cls._radios = yaml.safe_load(f) - @unittest.skipIf(os.getenv("CI"),("not working even if links are correct")) + @unittest.skipIf(os.getenv("CI"), ("not working even if links are correct")) async def test_radio_available(self): with disable_logging(): for k, v in self._radios.items():