Merge branch 'beta' into main

This commit is contained in:
Dominik
2025-01-11 15:36:45 +01:00
committed by GitHub
30 changed files with 567 additions and 279 deletions

52
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,52 @@
name: Bug Report
description: Report broken or incorrect behaviour
labels: unconfirmed bug
body:
- type: markdown
attributes:
value: >
Thank you for submitting a bug report! For real-time support, please join our [Discord community](https://discord.gg/wRCgB7vBQv).
This form is specifically for reporting bugs, and we appreciate your understanding!
**Note:** This form is for bugs only!
- type: input
attributes:
label: Summary
description: A simple summary of your bug report
validations:
required: true
- type: textarea
attributes:
label: Reproduction Steps
description: What you did to make it happen.
validations:
required: true
- type: textarea
attributes:
label: System Information
description: >
Run `python -m discord -v` and paste this information below. This command requires v1.1.0 or higher of the library.
If this errors out, please provide basic information about your system, such as your operating system and Python version.
validations:
required: true
- type: textarea
attributes:
label: Error Logs
description: Paste the loggings from your console. Include only relevant errors or warnings.
validations:
required: true
- type: checkboxes
attributes:
label: Checklist
description: Let's ensure you've done your due diligence when reporting this issue!
options:
- label: I have searched the open issues for duplicates.
required: true
- label: I have included the entire traceback, if possible.
required: true
- label: I have removed my token from display, if visible.
required: true
- type: textarea
attributes:
label: Additional Context
description: If there is anything else to say, please do so here.

View File

@@ -49,6 +49,7 @@ Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-US) AppleWebKit/530.6 (KHTM
Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/ Safari/530.5''' Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/ Safari/530.5'''
LYRIST_ENDPOINT = "https://lyrist.vercel.app/api/" LYRIST_ENDPOINT = "https://lyrist.vercel.app/api/"
LRCLIB_ENDPOINT = "https://lrclib.net/api/"
class LyricsPlatform(ABC): class LyricsPlatform(ABC):
@abstractmethod @abstractmethod
@@ -196,8 +197,26 @@ class Lyrist(LyricsPlatform):
except: except:
return None return None
class Lrclib(LyricsPlatform):
async def get(self, url, params: dict = None) -> list[dict]:
try:
async with aiohttp.ClientSession() as session:
resp = await session.get(url=url, headers={'User-Agent': random.choice(userAgents)}, params=params)
if resp.status != 200:
return None
return await resp.json()
except:
return []
async def get_lyrics(self, title, artist):
params = {"q": f"{title} - {artist}"}
result = await self.get(LRCLIB_ENDPOINT + "search", params)
if result:
return {"default": result[0].get("plainLyrics", "")}
lyricsPlatform: dict[str, LyricsPlatform] = { lyricsPlatform: dict[str, LyricsPlatform] = {
"a_zlyrics": A_ZLyrics, "a_zlyrics": A_ZLyrics,
"genius": Genius, "genius": Genius,
"lyrist": Lyrist "lyrist": Lyrist,
"lrclib": Lrclib
} }

View File

@@ -40,17 +40,11 @@ from function import (
logger logger
) )
from voicelink import SearchType, LoopType
from addons import lyricsPlatform from addons import lyricsPlatform
from views import SearchView, ListView, LinkView, LyricsView, HelpView from views import SearchView, ListView, LinkView, LyricsView, HelpView
from validators import url from validators import url
searchPlatform = {
"youtube": "ytsearch",
"youtubemusic": "ytmsearch",
"soundcloud": "scsearch",
"apple": "amsearch",
}
async def nowplay(ctx: commands.Context, player: voicelink.Player): async def nowplay(ctx: commands.Context, player: voicelink.Player):
track = player.current track = player.current
if not track: if not track:
@@ -73,7 +67,7 @@ async def nowplay(ctx: commands.Context, player: voicelink.Player):
icon = ":red_circle:" if track.is_stream else (":pause_button:" if player.is_paused else ":arrow_forward:") icon = ":red_circle:" if track.is_stream else (":pause_button:" if player.is_paused else ":arrow_forward:")
embed.add_field(name="\u2800", value=f"{icon} {pbar} **[{ctime(player.position)}/{track.formatted_length}]**", inline=False) embed.add_field(name="\u2800", value=f"{icon} {pbar} **[{ctime(player.position)}/{track.formatted_length}]**", inline=False)
return await ctx.send(embed=embed, view=LinkView(texts[2].format(track.source), track.emoji, track.uri)) return await send(ctx, embed, view=LinkView(texts[2].format(track.source), track.emoji, track.uri))
class Basic(commands.Cog): class Basic(commands.Cog):
def __init__(self, bot: commands.Bot) -> None: def __init__(self, bot: commands.Bot) -> None:
@@ -94,22 +88,22 @@ class Basic(commands.Cog):
async def play_autocomplete(self, interaction: discord.Interaction, current: str) -> list: async def play_autocomplete(self, interaction: discord.Interaction, current: str) -> list:
if voicelink.pool.URL_REGEX.match(current): return [app_commands.Choice(name=current, value=current)] if voicelink.pool.URL_REGEX.match(current): return [app_commands.Choice(name=current, value=current)]
if current:
node = voicelink.NodePool.get_node()
if node and node.spotify_client:
try:
tracks: list[voicelink.Track] = await node.spotifySearch(current, requester=interaction.user)
return [app_commands.Choice(name=truncate_string(f"🎵 {track.author} - {track.title}", 100), value=truncate_string(f"{track.author} - {track.title}", 100)) for track in tracks]
except voicelink.TrackLoadError:
return []
history: dict[str, str] = {} history: dict[str, str] = {}
for track_id in reversed(await get_user(interaction.user.id, "history")): for track_id in reversed(await get_user(interaction.user.id, "history")):
track_dict = voicelink.decode(track_id) track_dict = voicelink.decode(track_id)
history[track_dict["identifier"]] = track_dict history[track_dict["identifier"]] = track_dict
history_tracks = [app_commands.Choice(name=truncate_string(f"🕒 {track['author']} - {track['title']}", 100), value=track['uri']) for track in history.values() if len(track['uri']) <= 100][:25] history_tracks = [app_commands.Choice(name=truncate_string(f"🕒 {track['author']} - {track['title']}", 100), value=track['uri']) for track in history.values() if len(track['uri']) <= 100][:25]
if not current: return history_tracks
return history_tracks
node = voicelink.NodePool.get_node()
if node and node.spotify_client:
try:
tracks: list[voicelink.Track] = await node.spotifySearch(current, requester=interaction.user)
return [app_commands.Choice(name=truncate_string(f"🎵 {track.author} - {track.title}", 100), value=truncate_string(f"{track.author} - {track.title}", 100)) for track in tracks]
except voicelink.TrackLoadError:
return []
@commands.hybrid_command(name="connect", aliases=get_aliases("connect")) @commands.hybrid_command(name="connect", aliases=get_aliases("connect"))
@app_commands.describe(channel="Provide a channel to connect.") @app_commands.describe(channel="Provide a channel to connect.")
@@ -154,9 +148,16 @@ class Basic(commands.Cog):
else: else:
position = await player.add_track(tracks[0], start_time=format_time(start), end_time=format_time(end)) position = await player.add_track(tracks[0], start_time=format_time(start), end_time=format_time(end))
texts = await get_lang(ctx.guild.id, "live", "trackLoad_pos", "trackLoad") texts = await get_lang(ctx.guild.id, "live", "trackLoad_pos", "trackLoad")
await ctx.send((f"`{texts[0]}`" if tracks[0].is_stream else "") + (texts[1].format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length, position) if position >= 1 and player.is_playing else texts[2].format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length)), allowed_mentions=False)
except voicelink.QueueFull as e: stream_content = f"`{texts[0]}`" if tracks[0].is_stream else ""
await ctx.send(e) additional_content = texts[1] if position >= 1 and player.is_playing else texts[2]
await send(
ctx,
stream_content + additional_content,
tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length,
position if position >= 1 and player.is_playing else None
)
finally: finally:
if not player.is_playing: if not player.is_playing:
await player.do_next() await player.do_next()
@@ -195,10 +196,16 @@ class Basic(commands.Cog):
else: else:
position = await player.add_track(tracks[0]) position = await player.add_track(tracks[0])
texts = await get_lang(interaction.guild.id, "live", "trackLoad_pos", "trackLoad") texts = await get_lang(interaction.guild.id, "live", "trackLoad_pos", "trackLoad")
await interaction.followup.send((f"`{texts[0]}`" if tracks[0].is_stream else "") + (texts[1].format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length, position) if position >= 1 and player.is_playing else texts[2].format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length)), allowed_mentions=False)
except voicelink.QueueFull as e:
await interaction.followup.send(e)
stream_content = f"`{texts[0]}`" if tracks[0].is_stream else ""
additional_content = texts[1] if position >= 1 and player.is_playing else texts[2]
await send(
interaction,
stream_content + additional_content,
tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length,
position if position >= 1 and player.is_playing else None
)
finally: finally:
if not player.is_playing: if not player.is_playing:
await player.do_next() await player.do_next()
@@ -209,14 +216,11 @@ class Basic(commands.Cog):
platform="Select the platform you want to search." platform="Select the platform you want to search."
) )
@app_commands.choices(platform=[ @app_commands.choices(platform=[
app_commands.Choice(name="Youtube", value="Youtube"), app_commands.Choice(name=search_type.name.replace("_", " ").title(), value=search_type.name)
app_commands.Choice(name="Youtube Music", value="YoutubeMusic"), for search_type in SearchType
app_commands.Choice(name="Spotify", value="Spotify"),
app_commands.Choice(name="SoundCloud", value="SoundCloud"),
app_commands.Choice(name="Apple Music", value="Apple")
]) ])
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
async def search(self, ctx: commands.Context, *, query: str, platform: str = "Youtube"): async def search(self, ctx: commands.Context, *, query: str, platform: str = SearchType.YOUTUBE.name):
"Loads your input and added it to the queue." "Loads your input and added it to the queue."
player: voicelink.Player = ctx.guild.voice_client player: voicelink.Player = ctx.guild.voice_client
if not player: if not player:
@@ -227,14 +231,8 @@ class Basic(commands.Cog):
if url(query): if url(query):
return await send(ctx, "noLinkSupport", ephemeral=True) return await send(ctx, "noLinkSupport", ephemeral=True)
platform = platform.lower() tracks = await player.get_tracks(query=query, requester=ctx.author, search_type=SearchType[platform] if platform in SearchType.__members__ else SearchType.YOUTUBE)
if platform != 'spotify':
query_platform = searchPlatform.get(platform, 'ytsearch') + f":{query}"
tracks = await player.get_tracks(query=query_platform, requester=ctx.author)
else:
tracks = await player.node.spotifySearch(query=query, requester=ctx.author)
if not tracks: if not tracks:
return await send(ctx, "noTrackFound") return await send(ctx, "noTrackFound")
@@ -242,7 +240,7 @@ class Basic(commands.Cog):
query_track = "\n".join(f"`{index}.` `[{track.formatted_length}]` **{track.title[:35]}**" for index, track in enumerate(tracks[0:10], start=1)) query_track = "\n".join(f"`{index}.` `[{track.formatted_length}]` **{track.title[:35]}**" for index, track in enumerate(tracks[0:10], start=1))
embed = discord.Embed(title=texts[0].format(query), description=texts[1].format(get_source(platform, "emoji"), platform, len(tracks[0:10]), query_track), color=settings.embed_color) embed = discord.Embed(title=texts[0].format(query), description=texts[1].format(get_source(platform, "emoji"), platform, len(tracks[0:10]), query_track), color=settings.embed_color)
view = SearchView(tracks=tracks[0:10], texts=[texts[5], texts[6]]) view = SearchView(tracks=tracks[0:10], texts=[texts[5], texts[6]])
view.response = await ctx.send(embed=embed, view=view, ephemeral=True) view.response = await send(ctx, embed, view=view, ephemeral=True)
await view.wait() await view.wait()
if view.values is not None: if view.values is not None:
@@ -251,7 +249,7 @@ class Basic(commands.Cog):
track = tracks[int(value.split(". ")[0]) - 1] track = tracks[int(value.split(". ")[0]) - 1]
position = await player.add_track(track) position = await player.add_track(track)
msg += (f"`{texts[2]}`" if track.is_stream else "") + (texts[3].format(track.title, track.uri, track.author, track.formatted_length, position) if position >= 1 else texts[4].format(track.title, track.uri, track.author, track.formatted_length)) msg += (f"`{texts[2]}`" if track.is_stream else "") + (texts[3].format(track.title, track.uri, track.author, track.formatted_length, position) if position >= 1 else texts[4].format(track.title, track.uri, track.author, track.formatted_length))
await ctx.send(msg, allowed_mentions=False) await send(ctx, msg)
if not player.is_playing: if not player.is_playing:
await player.do_next() await player.do_next()
@@ -287,11 +285,16 @@ class Basic(commands.Cog):
else: else:
position = await player.add_track(tracks[0], start_time=format_time(start), end_time=format_time(end), at_front=True) position = await player.add_track(tracks[0], start_time=format_time(start), end_time=format_time(end), at_front=True)
texts = await get_lang(ctx.guild.id, "live", "trackLoad_pos", "trackLoad") texts = await get_lang(ctx.guild.id, "live", "trackLoad_pos", "trackLoad")
await ctx.send((f"`{texts[0]}`" if tracks[0].is_stream else "") + (texts[1].format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length, position) if position >= 1 and player.is_playing else texts[2].format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length)), allowed_mentions=False)
except voicelink.QueueFull as e:
await ctx.send(e)
stream_content = f"`{texts[0]}`" if tracks[0].is_stream else ""
additional_content = texts[1] if position >= 1 and player.is_playing else texts[2]
await send(
ctx,
stream_content + additional_content,
tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length,
position if position >= 1 and player.is_playing else None
)
finally: finally:
if not player.is_playing: if not player.is_playing:
await player.do_next() await player.do_next()
@@ -326,14 +329,17 @@ class Basic(commands.Cog):
else: else:
texts = await get_lang(ctx.guild.id, "live", "trackLoad") texts = await get_lang(ctx.guild.id, "live", "trackLoad")
await player.add_track(tracks[0], start_time=format_time(start), end_time=format_time(end), at_front=True) await player.add_track(tracks[0], start_time=format_time(start), end_time=format_time(end), at_front=True)
await ctx.send((f"`{texts[0]}`" if tracks[0].is_stream else "") + texts[1].format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length), allowed_mentions=False)
except voicelink.QueueFull as e: stream_content = f"`{texts[0]}`" if tracks[0].is_stream else ""
await ctx.send(e)
await send(
ctx,
stream_content + texts[1],
tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length,
)
finally: finally:
if player.queue._repeat.mode == voicelink.LoopType.track: if player.queue._repeat.mode == voicelink.LoopType.TRACK:
await player.set_repeat(voicelink.LoopType.off.name) await player.set_repeat(voicelink.LoopType.OFF)
await player.stop() if player.is_playing else await player.do_next() await player.stop() if player.is_playing else await player.do_next()
@@ -410,8 +416,8 @@ class Basic(commands.Cog):
player.queue.skipto(index) player.queue.skipto(index)
await send(ctx, "skipped", ctx.author) await send(ctx, "skipped", ctx.author)
if player.queue._repeat.mode == voicelink.LoopType.track: if player.queue._repeat.mode == voicelink.LoopType.TRACK:
await player.set_repeat(voicelink.LoopType.off.name) await player.set_repeat(voicelink.LoopType.OFF)
await player.stop() await player.stop()
@@ -443,8 +449,8 @@ class Basic(commands.Cog):
await player.stop() await player.stop()
await send(ctx, "backed", ctx.author) await send(ctx, "backed", ctx.author)
if player.queue._repeat.mode == voicelink.LoopType.track: if player.queue._repeat.mode == voicelink.LoopType.TRACK:
await player.set_repeat(voicelink.LoopType.off.name) await player.set_repeat(voicelink.LoopType.OFF)
@commands.hybrid_command(name="seek", aliases=get_aliases("seek")) @commands.hybrid_command(name="seek", aliases=get_aliases("seek"))
@app_commands.describe(position="Input position. Exmaple: 1:20.") @app_commands.describe(position="Input position. Exmaple: 1:20.")
@@ -486,7 +492,7 @@ class Basic(commands.Cog):
if player.queue.is_empty: if player.queue.is_empty:
return await nowplay(ctx, player) return await nowplay(ctx, player)
view = ListView(player=player, author=ctx.author) view = ListView(player=player, author=ctx.author)
view.response = await ctx.send(embed=await view.build_embed(), view=view) view.response = await send(ctx, await view.build_embed(), view=view)
@queue.command(name="export", aliases=get_aliases("export")) @queue.command(name="export", aliases=get_aliases("export"))
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
@@ -547,10 +553,6 @@ class Basic(commands.Cog):
index = await player.add_track(tracks) index = await player.add_track(tracks)
await send(ctx, "playlistLoad", attachment.filename, index) await send(ctx, "playlistLoad", attachment.filename, index)
except voicelink.QueueFull as e:
return await ctx.send(e, ephemeral=True)
except Exception as e: except Exception as e:
logger.error("error", exc_info=e) logger.error("error", exc_info=e)
raise e raise e
@@ -574,7 +576,7 @@ class Basic(commands.Cog):
return await nowplay(ctx, player) return await nowplay(ctx, player)
view = ListView(player=player, author=ctx.author, is_queue=False) view = ListView(player=player, author=ctx.author, is_queue=False)
view.response = await ctx.send(embed=await view.build_embed(), view=view) view.response = await send(ctx, await view.build_embed(), view=view)
@commands.hybrid_command(name="leave", aliases=get_aliases("leave")) @commands.hybrid_command(name="leave", aliases=get_aliases("leave"))
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
@@ -613,9 +615,8 @@ class Basic(commands.Cog):
@commands.hybrid_command(name="loop", aliases=get_aliases("loop")) @commands.hybrid_command(name="loop", aliases=get_aliases("loop"))
@app_commands.describe(mode="Choose a looping mode.") @app_commands.describe(mode="Choose a looping mode.")
@app_commands.choices(mode=[ @app_commands.choices(mode=[
app_commands.Choice(name='Off', value='off'), app_commands.Choice(name=loop_type.name.title(), value=loop_type.name)
app_commands.Choice(name='Track', value='track'), for loop_type in LoopType
app_commands.Choice(name='Queue', value='queue')
]) ])
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
async def loop(self, ctx: commands.Context, mode: str): async def loop(self, ctx: commands.Context, mode: str):
@@ -627,7 +628,7 @@ class Basic(commands.Cog):
if not player.is_privileged(ctx.author): if not player.is_privileged(ctx.author):
return await send(ctx, "missingPerms_mode", ephemeral=True) return await send(ctx, "missingPerms_mode", ephemeral=True)
await player.set_repeat(mode, ctx.author) await player.set_repeat(LoopType[mode] if mode in LoopType.__members__ else LoopType.OFF, ctx.author)
await send(ctx, "repeat", mode.capitalize()) await send(ctx, "repeat", mode.capitalize())
@commands.hybrid_command(name="clear", aliases=get_aliases("clear")) @commands.hybrid_command(name="clear", aliases=get_aliases("clear"))
@@ -669,7 +670,7 @@ class Basic(commands.Cog):
await send(ctx, "removed", len(removed_tracks.keys())) await send(ctx, "removed", len(removed_tracks.keys()))
@commands.hybrid_command(name="forward", aliases=get_aliases("forward")) @commands.hybrid_command(name="forward", aliases=get_aliases("forward"))
@app_commands.describe(position="Input a amount that you to forward to. Exmaple: 1:20") @app_commands.describe(position="Input an amount that you to forward to. Exmaple: 1:20")
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
async def forward(self, ctx: commands.Context, position: str = "10"): async def forward(self, ctx: commands.Context, position: str = "10"):
"Forwards by a certain amount of time in the current track. The default is 10 seconds." "Forwards by a certain amount of time in the current track. The default is 10 seconds."
@@ -690,7 +691,7 @@ class Basic(commands.Cog):
await send(ctx, "forward", ctime(player.position + num)) await send(ctx, "forward", ctime(player.position + num))
@commands.hybrid_command(name="rewind", aliases=get_aliases("rewind")) @commands.hybrid_command(name="rewind", aliases=get_aliases("rewind"))
@app_commands.describe(position="Input a amount that you to rewind to. Exmaple: 1:20") @app_commands.describe(position="Input an amount that you to rewind to. Exmaple: 1:20")
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
async def rewind(self, ctx: commands.Context, position: str = "10"): async def rewind(self, ctx: commands.Context, position: str = "10"):
"Rewind by a certain amount of time in the current track. The default is 10 seconds." "Rewind by a certain amount of time in the current track. The default is 10 seconds."
@@ -801,7 +802,7 @@ class Basic(commands.Cog):
return await send(ctx, "lyricsNotFound", ephemeral=True) return await send(ctx, "lyricsNotFound", ephemeral=True)
view = LyricsView(name=title, source={_: re.findall(r'.*\n(?:.*\n){,22}', v) for _, v in song.items()}, author=ctx.author) view = LyricsView(name=title, source={_: re.findall(r'.*\n(?:.*\n){,22}', v) for _, v in song.items()}, author=ctx.author)
view.response = await ctx.send(embed=view.build_embed(), view=view) view.response = await send(ctx, view.build_embed(), view=view)
@commands.hybrid_command(name="swapdj", aliases=get_aliases("swapdj")) @commands.hybrid_command(name="swapdj", aliases=get_aliases("swapdj"))
@app_commands.describe(member="Choose a member to transfer the dj role.") @app_commands.describe(member="Choose a member to transfer the dj role.")
@@ -857,7 +858,7 @@ class Basic(commands.Cog):
category = "News" category = "News"
view = HelpView(self.bot, ctx.author) view = HelpView(self.bot, ctx.author)
embed = view.build_embed(category) embed = view.build_embed(category)
view.response = await ctx.send(embed=embed, view=view) view.response = await send(ctx, embed, view=view)
@commands.hybrid_command(name="ping", aliases=get_aliases("ping")) @commands.hybrid_command(name="ping", aliases=get_aliases("ping"))
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
@@ -882,7 +883,7 @@ class Basic(commands.Cog):
inline=False inline=False
) )
await ctx.send(embed=embed) await send(ctx, embed)
async def setup(bot: commands.Bot) -> None: async def setup(bot: commands.Bot) -> None:
await bot.add_cog(Basic(bot)) await bot.add_cog(Basic(bot))

View File

@@ -107,7 +107,7 @@ class Playlists(commands.Cog, name="playlist"):
async def playlist(self, ctx: commands.Context): async def playlist(self, ctx: commands.Context):
view = HelpView(self.bot, ctx.author) view = HelpView(self.bot, ctx.author)
embed = view.build_embed(self.qualified_name) embed = view.build_embed(self.qualified_name)
view.response = await ctx.send(embed=embed, view=view) view.response = send(ctx, embed, view=view)
@playlist.command(name="play", aliases=get_aliases("play")) @playlist.command(name="play", aliases=get_aliases("play"))
@app_commands.describe( @app_commands.describe(
@@ -212,7 +212,7 @@ class Playlists(commands.Cog, name="playlist"):
embed.set_footer(text=text[2]) embed.set_footer(text=text[2])
view = PlaylistView(embed, results, ctx.author) view = PlaylistView(embed, results, ctx.author)
view.response = await ctx.send(embed=embed, view=view, ephemeral=True) view.response = await send(ctx, embed, view=view, ephemeral=True)
@playlist.command(name="create", aliases=get_aliases("create")) @playlist.command(name="create", aliases=get_aliases("create"))
@app_commands.describe( @app_commands.describe(
@@ -344,7 +344,7 @@ class Playlists(commands.Cog, name="playlist"):
inbox = user['inbox'].copy() inbox = user['inbox'].copy()
view = InboxView(ctx.author, user['inbox']) view = InboxView(ctx.author, user['inbox'])
view.response = await ctx.send(embed=view.build_embed(), view=view, ephemeral=True) view.response = await send(ctx, view.build_embed(), view=view, ephemeral=True)
await view.wait() await view.wait()
if inbox == user['inbox']: if inbox == user['inbox']:

View File

@@ -58,13 +58,16 @@ class Settings(commands.Cog, name="settings"):
async def settings(self, ctx: commands.Context): async def settings(self, ctx: commands.Context):
view = HelpView(self.bot, ctx.author) view = HelpView(self.bot, ctx.author)
embed = view.build_embed(self.qualified_name) embed = view.build_embed(self.qualified_name)
view.response = await ctx.send(embed=embed, view=view) view.response = await send(ctx, embed, view=view)
@settings.command(name="prefix", aliases=get_aliases("prefix")) @settings.command(name="prefix", aliases=get_aliases("prefix"))
@commands.has_permissions(manage_guild=True) @commands.has_permissions(manage_guild=True)
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
async def prefix(self, ctx: commands.Context, prefix: str): async def prefix(self, ctx: commands.Context, prefix: str):
"Change the default prefix for message commands." "Change the default prefix for message commands."
if not self.bot.intents.message_content:
return await send(ctx, "missingIntents", "MESSAGE_CONTENT", ephemeral=True)
await update_settings(ctx.guild.id, {"$set": {"prefix": prefix}}) await update_settings(ctx.guild.id, {"$set": {"prefix": prefix}})
await send(ctx, "setPrefix", prefix, prefix) await send(ctx, "setPrefix", prefix, prefix)
@@ -170,7 +173,7 @@ class Settings(commands.Cog, name="settings"):
), ),
inline=False inline=False
) )
await ctx.send(embed=embed) await send(ctx, embed)
@settings.command(name="volume", aliases=get_aliases("volume")) @settings.command(name="volume", aliases=get_aliases("volume"))
@app_commands.describe(value="Input a integer.") @app_commands.describe(value="Input a integer.")
@@ -226,7 +229,7 @@ class Settings(commands.Cog, name="settings"):
controller_settings = settings.get("default_controller", func.settings.controller) controller_settings = settings.get("default_controller", func.settings.controller)
view = EmbedBuilderView(ctx, controller_settings.get("embeds").copy()) view = EmbedBuilderView(ctx, controller_settings.get("embeds").copy())
view.response = await ctx.send(embed=view.build_embed(), view=view) view.response = await send(ctx, view.build_embed(), view=view)
@settings.command(name="controllermsg", aliases=get_aliases("controllermsg")) @settings.command(name="controllermsg", aliases=get_aliases("controllermsg"))
@commands.has_permissions(manage_guild=True) @commands.has_permissions(manage_guild=True)
@@ -243,9 +246,46 @@ class Settings(commands.Cog, name="settings"):
@commands.has_permissions(manage_guild=True) @commands.has_permissions(manage_guild=True)
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
async def stageannounce(self, ctx: commands.Context, template: str = None): async def stageannounce(self, ctx: commands.Context, template: str = None):
"""Customize the channel topic template""" "Customize the channel topic template"
await update_settings(ctx.guild.id, {"$set": {'stage_announce_template': template}}) await update_settings(ctx.guild.id, {"$set": {'stage_announce_template': template}})
await send(ctx, "SetStageAnnounceTemplate") await send(ctx, "setStageAnnounceTemplate")
@settings.command(name="setupchannel", aliases=get_aliases("setupchannel"))
@app_commands.describe(
channel="Provide a request channel. If not, a text channel will be generated."
)
@commands.has_permissions(manage_guild=True)
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
async def setupchannel(self, ctx: commands.Context, channel: discord.TextChannel = None) -> None:
"Sets up a dedicated channel for song requests in your server."
if not self.bot.intents.message_content:
return await send(ctx, "missingIntents", "MESSAGE_CONTENT", ephemeral=True)
if not channel:
try:
overwrites = {
ctx.guild.me: discord.PermissionOverwrite(
read_messages=True,
manage_messages=True
)
}
channel = await ctx.guild.create_text_channel("vocard-song-requests", overwrites=overwrites)
except:
return await send(ctx, "noCreatePermission")
channel_perms = channel.permissions_for(ctx.me)
if not channel_perms.text() and not channel_perms.manage_messages:
return await send(ctx, "noCreatePermission")
settings = await func.get_settings(ctx.guild.id)
controller = settings.get("default_controller", func.settings.controller).get("embeds", {}).get("inactive", {})
message = await channel.send(embed=voicelink.build_embed(controller, voicelink.Placeholders(self.bot)))
await update_settings(ctx.guild.id, {"$set": {'music_request_channel': {
"text_channel_id": channel.id,
"controller_msg_id": message.id,
}}})
await send(ctx, "createSongRequestChannel", channel.mention)
@app_commands.command(name="debug") @app_commands.command(name="debug")
async def debug(self, interaction: discord.Interaction): async def debug(self, interaction: discord.Interaction):
@@ -268,7 +308,7 @@ class Settings(commands.Cog, name="settings"):
value=f"```• VERSION: {func.settings.version}\n" \ value=f"```• VERSION: {func.settings.version}\n" \
f"• LATENCY: {self.bot.latency:.2f}ms\n" \ f"• LATENCY: {self.bot.latency:.2f}ms\n" \
f"• GUILDS: {len(self.bot.guilds)}\n" \ f"• GUILDS: {len(self.bot.guilds)}\n" \
f"• USERS: {sum([guild.member_count for guild in self.bot.guilds])}\n" \ f"• USERS: {sum([guild.member_count or 0 for guild in self.bot.guilds])}\n" \
f"• PLAYERS: {len(self.bot.voice_clients)}```", f"• PLAYERS: {len(self.bot.voice_clients)}```",
inline=False inline=False
) )

View File

@@ -34,6 +34,8 @@ LOCAL_LANGS: dict[str, dict[str, str]] = {} #Stores all the localization languag
SETTINGS_BUFFER: dict[int, dict[str, Any]] = {} #Cache guild language SETTINGS_BUFFER: dict[int, dict[str, Any]] = {} #Cache guild language
USERS_BUFFER: dict[str, dict] = {} USERS_BUFFER: dict[str, dict] = {}
MISSING_TRANSLATOR: dict[str, list[str]] = {}
USER_BASE: dict[str, Any] = { USER_BASE: dict[str, Any] = {
'playlist': { 'playlist': {
'200': { '200': {
@@ -106,7 +108,7 @@ def format_time(number:str) -> int:
return (int(num.tm_hour) * 3600 + int(num.tm_min) * 60 + int(num.tm_sec)) * 1000 return (int(num.tm_hour) * 3600 + int(num.tm_min) * 60 + int(num.tm_sec)) * 1000
def get_source(source: str, type: str) -> str: def get_source(source: str, type: str) -> str:
source_settings: dict = settings.sources_settings.get(source.lower(), {}) source_settings: dict = settings.sources_settings.get(source.lower(), settings.sources_settings.get("others"))
return source_settings.get(type, ("🔗" if type == "emoji" else settings.embed_color)) return source_settings.get(type, ("🔗" if type == "emoji" else settings.embed_color))
def cooldown_check(ctx: commands.Context) -> Optional[commands.Cooldown]: def cooldown_check(ctx: commands.Context) -> Optional[commands.Cooldown]:
@@ -143,30 +145,55 @@ def format_bytes(bytes: int, unit: bool = False):
else: else:
return f"{bytes / (1024 ** 3):.1f}" + ("GB" if unit else "") return f"{bytes / (1024 ** 3):.1f}" + ("GB" if unit else "")
async def get_lang(guild_id:int, *keys) -> Union[list[str], str]: async def get_lang(guild_id:int, *keys) -> Optional[Union[list[str], str]]:
settings = await get_settings(guild_id) settings = await get_settings(guild_id)
lang = settings.get("lang", "EN") lang = settings.get("lang", "EN")
if lang in LANGS and not LANGS[lang]: if lang in LANGS and not LANGS[lang]:
LANGS[lang] = open_json(os.path.join("langs", f"{lang}.json")) LANGS[lang] = open_json(os.path.join("langs", f"{lang}.json"))
if len(keys) == 1: if len(keys) == 1:
return LANGS.get(lang, {}).get(keys[0], "Language pack not found!") return LANGS.get(lang, {}).get(keys[0])
return [LANGS.get(lang, {}).get(key, "Language pack not found!") for key in keys] return [LANGS.get(lang, {}).get(key) for key in keys]
async def send(ctx: Union[commands.Context, discord.Interaction], key: str, *params, delete_after: float = None, ephemeral: bool = False) -> Optional[discord.Message]: async def send(
text = await get_lang(ctx.guild.id, key) ctx: Union[commands.Context, discord.Interaction],
text = text.format(*params) content: Union[str, discord.Embed] = None,
*params,
view: discord.ui.View = None,
delete_after: float = None,
ephemeral: bool = False
) -> Optional[discord.Message]:
if content is None:
content = "No content provided."
if isinstance(ctx, commands.Context): # Determine the text to send
send_func = ctx.send if isinstance(content, discord.Embed):
embed = content
text = None
else: else:
if not ctx.response.is_done(): text = await get_lang(ctx.guild.id, content)
send_func = ctx.response.send_message if text:
text = text.format(*params)
else: else:
return await ctx.followup.send(text, ephemeral=ephemeral, allowed_mentions=ALLOWED_MENTIONS) text = content.format(*params)
embed = None
return await send_func(text, delete_after=delete_after, ephemeral=ephemeral, allowed_mentions=ALLOWED_MENTIONS) # Determine the sending function
send_func = (
ctx.send if isinstance(ctx, commands.Context) else
ctx.response.send_message if not ctx.response.is_done() else
ctx.followup.send
)
# Check settings for delete_after duration
settings = await get_settings(ctx.guild.id)
if settings and ctx.channel.id == settings.get("music_request_channel", {}).get("text_channel_id"):
delete_after = 10
# Send the message or embed
if view:
return await send_func(text, embed=embed, view=view, delete_after=delete_after, ephemeral=ephemeral, allowed_mentions=ALLOWED_MENTIONS)
return await send_func(text, embed=embed, delete_after=delete_after, ephemeral=ephemeral, allowed_mentions=ALLOWED_MENTIONS)
async def update_db(db: AsyncIOMotorCollection, tempStore: dict, filter: dict, data: dict) -> bool: async def update_db(db: AsyncIOMotorCollection, tempStore: dict, filter: dict, data: dict) -> bool:
for mode, action in data.items(): for mode, action in data.items():

View File

@@ -178,8 +178,8 @@ async def skipTo(player: Player, member: Member, data: Dict) -> None:
if index > 1: if index > 1:
player.queue.skipto(index) player.queue.skipto(index)
if player.queue._repeat.mode == LoopType.track: if player.queue._repeat.mode == LoopType.TRACK:
await player.set_repeat(LoopType.off.name) await player.set_repeat(LoopType.OFF)
await player.stop() await player.stop()
async def backTo(player: Player, member: Member, data: Dict) -> None: async def backTo(player: Player, member: Member, data: Dict) -> None:

View File

@@ -7,9 +7,11 @@
"noChannel": "沒有語音頻道可供連接。請提供一個語音頻道或加入一個語音頻道。", "noChannel": "沒有語音頻道可供連接。請提供一個語音頻道或加入一個語音頻道。",
"alreadyConnected": "已經連接到語音頻道。", "alreadyConnected": "已經連接到語音頻道。",
"noPermission": "抱歉!我沒有權限加入或在您的語音頻道中發言。", "noPermission": "抱歉!我沒有權限加入或在您的語音頻道中發言。",
"noCreatePermission": "抱歉!我沒有權限建立歌曲請求頻道。",
"noPlaySource": "找不到任何可播放的來源!", "noPlaySource": "找不到任何可播放的來源!",
"noPlayer": "在此伺服器上找不到播放器。", "noPlayer": "在此伺服器上找不到播放器。",
"notVote": "此命令需要您的投票!輸入 `/vote` 以獲取更多資訊。", "notVote": "此命令需要您的投票!輸入 `/vote` 以獲取更多資訊。",
"missingIntents": "抱歉,此命令無法執行,因為機器人缺少所需的請求意圖:`({0})`.",
"languageNotFound": "找不到語言包!請選擇一個現有的語言包。", "languageNotFound": "找不到語言包!請選擇一個現有的語言包。",
"changedLanguage": "已成功切換到 `{0}` 語言包。", "changedLanguage": "已成功切換到 `{0}` 語言包。",
"setPrefix": "完成!我的前綴在您的伺服器中現在是 `{0}`。嘗試運行 `{1}ping` 來測試它。", "setPrefix": "完成!我的前綴在您的伺服器中現在是 `{0}`。嘗試運行 `{1}ping` 來測試它。",
@@ -186,5 +188,5 @@
"invalidEndTime": "無效的結束時間! 時間必須在 `00:00` 和 `{0}` 之間。", "invalidEndTime": "無效的結束時間! 時間必須在 `00:00` 和 `{0}` 之間。",
"invalidTimeOrder": "結束時間不能小於或等於開始時間。", "invalidTimeOrder": "結束時間不能小於或等於開始時間。",
"SetStageAnnounceTemplate": "完成!從現在開始,像您現在的語音狀態將根據您的模板命名。您應該在幾秒鐘內看到它更新。" "setStageAnnounceTemplate": "完成!從現在開始,像您現在的語音狀態將根據您的模板命名。您應該在幾秒鐘內看到它更新。"
} }

View File

@@ -7,9 +7,11 @@
"noChannel": "Kein Sprachkanal zum Verbinden gefunden. Bitte stelle entweder einen zur Verfügung oder trete einem bei.", "noChannel": "Kein Sprachkanal zum Verbinden gefunden. Bitte stelle entweder einen zur Verfügung oder trete einem bei.",
"alreadyConnected": "Bereits mit einem Sprachkanal verbunden.", "alreadyConnected": "Bereits mit einem Sprachkanal verbunden.",
"noPermission": "Es tut mir leid, ich bin nicht berechtigt, dem Sprachkanal beizutreten oder darin zu sprechen.", "noPermission": "Es tut mir leid, ich bin nicht berechtigt, dem Sprachkanal beizutreten oder darin zu sprechen.",
"noCreatePermission": "Es tut mir leid, ich habe keine Berechtigung, einen Song Request Channel zu erstellen.",
"noPlaySource": "Ich kann keine abspielbaren Quellen finden!", "noPlaySource": "Ich kann keine abspielbaren Quellen finden!",
"noPlayer": "Auf diesem Server wurden kein Player gefunden.", "noPlayer": "Auf diesem Server wurden kein Player gefunden.",
"notVote": "Dieser Befehl erfordert Deine Stimme! Gebe `/vote` ein, um weitere Informationen zu erhalten.", "notVote": "Dieser Befehl erfordert Deine Stimme! Gebe `/vote` ein, um weitere Informationen zu erhalten.",
"missingIntents": "Es tut mir leid, dieser Befehl kann nicht ausgeführt werden, da mir der `({0})` Intent fehlt.",
"languageNotFound": "Kein Sprachpaket gefunden. Bitte wähle ein vorhandenes Sprachpaket aus.", "languageNotFound": "Kein Sprachpaket gefunden. Bitte wähle ein vorhandenes Sprachpaket aus.",
"changedLanguage": "Erfolgreich auf das Sprachpaket `{0}` geändert.", "changedLanguage": "Erfolgreich auf das Sprachpaket `{0}` geändert.",
"setPrefix": "Erledigt! Mein Präfix ist jetzt `{0}` auf deinem Server. Versuche, `{1}ping` auszuführen, um es zu testen.", "setPrefix": "Erledigt! Mein Präfix ist jetzt `{0}` auf deinem Server. Versuche, `{1}ping` auszuführen, um es zu testen.",
@@ -187,4 +189,4 @@
"invalidTimeOrder": "Die Endzeit darf nicht kleiner oder gleich dem Startzeit sein.", "invalidTimeOrder": "Die Endzeit darf nicht kleiner oder gleich dem Startzeit sein.",
"SetStageAnnounceTemplate": "Fertig! Ab sofort wird der Sprachstatus wie der, in dem Du Dich gerade befindest, gemäß Deiner Vorlage benannt. Du solltest in wenigen Sekunden eine Aktualisierung sehen." "SetStageAnnounceTemplate": "Fertig! Ab sofort wird der Sprachstatus wie der, in dem Du Dich gerade befindest, gemäß Deiner Vorlage benannt. Du solltest in wenigen Sekunden eine Aktualisierung sehen."
} }

View File

@@ -7,9 +7,11 @@
"noChannel": "No voice channel to connect. Please either provide one or join one.", "noChannel": "No voice channel to connect. Please either provide one or join one.",
"alreadyConnected": "Already connected to a voice channel.", "alreadyConnected": "Already connected to a voice channel.",
"noPermission": "Sorry! i don't have permissions to join or speak in your voice channel.", "noPermission": "Sorry! i don't have permissions to join or speak in your voice channel.",
"noCreatePermission": "Sorry! i don't have permissions to create a song requesting channel.",
"noPlaySource": "Can't found any playable sources!", "noPlaySource": "Can't found any playable sources!",
"noPlayer": "No player has found on this server.", "noPlayer": "No player has found on this server.",
"notVote": "This command requires your vote! Type `/vote` for more info.", "notVote": "This command requires your vote! Type `/vote` for more info.",
"missingIntents": "Sorry, this command cannot be executed because the bot is missing the required request intent: `({0})`.",
"languageNotFound": "No language pack found! please select an existing language pack.", "languageNotFound": "No language pack found! please select an existing language pack.",
"changedLanguage": "Successfully changed to `{0}` language pack.", "changedLanguage": "Successfully changed to `{0}` language pack.",
"setPrefix": "Done! My prefix in your server is now `{0}`. Try running `{1}ping` to test it out.", "setPrefix": "Done! My prefix in your server is now `{0}`. Try running `{1}ping` to test it out.",
@@ -124,7 +126,7 @@
"live": "LIVE", "live": "LIVE",
"playlistLoad": " 🎶 Added the playlist **{0}** with `{1}` songs to the queue.", "playlistLoad": " 🎶 Added the playlist **{0}** with `{1}` songs to the queue.",
"trackLoad": "Added **[{0}](<{1}>)** by **{2}** (`{3}`) to begin playing.\n", "trackLoad": "Added **[{0}](<{1}>)** by **{2}** (`{3}`) to begin playing.\n",
"trackLoad_pos": "Added **[{0}](<{1}>)** by **{3}** (`{3}`) to the queue at position **{4}**\n", "trackLoad_pos": "Added **[{0}](<{1}>)** by **{2}** (`{3}`) to the queue at position **{4}**\n",
"searchTitle": "Search Query: {0}", "searchTitle": "Search Query: {0}",
"searchDesc": "➥ Platform: {0} **{1}**\n➥ Results: **{2}**\n\n{3}", "searchDesc": "➥ Platform: {0} **{1}**\n➥ Results: **{2}**\n\n{3}",
@@ -186,5 +188,6 @@
"invalidEndTime": "Invalid end time, it must be between `00:00` and `{0}`", "invalidEndTime": "Invalid end time, it must be between `00:00` and `{0}`",
"invalidTimeOrder": "End time cannot be less than or equal to start time", "invalidTimeOrder": "End time cannot be less than or equal to start time",
"SetStageAnnounceTemplate": "Done! From now on, voice status like the one you're in now will be named according to your template. You should see it update in a few seconds." "setStageAnnounceTemplate": "Done! From now on, voice status like the one you're in now will be named according to your template. You should see it update in a few seconds.",
"createSongRequestChannel": "A song request channel ({0}) has been created! You can start requesting any song by name or URL in that channel, without needing to use the bot prefix."
} }

View File

@@ -7,9 +7,11 @@
"noChannel": "No hay canal de voz al que conectarse. Por favor, proporcione uno o únase a uno.", "noChannel": "No hay canal de voz al que conectarse. Por favor, proporcione uno o únase a uno.",
"alreadyConnected": "Ya conectado a un canal de voz.", "alreadyConnected": "Ya conectado a un canal de voz.",
"noPermission": "¡Lo siento! No tengo permisos para unirme o hablar en su canal de voz.", "noPermission": "¡Lo siento! No tengo permisos para unirme o hablar en su canal de voz.",
"noCreatePermission": "¡Lo siento! No tengo permisos para crear un canal de solicitud de canciones.",
"noPlaySource": "¡No se puede encontrar ninguna fuente reproducible!", "noPlaySource": "¡No se puede encontrar ninguna fuente reproducible!",
"noPlayer": "No se ha encontrado ningún reproductor en este servidor.", "noPlayer": "No se ha encontrado ningún reproductor en este servidor.",
"notVote": "¡Este comando requiere su voto! Escriba `/vote` para obtener más información.", "notVote": "¡Este comando requiere su voto! Escriba `/vote` para obtener más información.",
"missingIntents": "Lo siento, este comando no se puede ejecutar porque el bot carece de la intención de solicitud requerida: `({0})`.",
"languageNotFound": "¡No se encontró paquete de idioma! por favor seleccione un paquete de idioma existente.", "languageNotFound": "¡No se encontró paquete de idioma! por favor seleccione un paquete de idioma existente.",
"changedLanguage": "Cambiado con éxito al paquete de idioma `{0}`.", "changedLanguage": "Cambiado con éxito al paquete de idioma `{0}`.",
"setPrefix": "¡Listo! Mi prefijo en tu servidor ahora es `{0}`. Intenta ejecutar `{1}ping` para probarlo.", "setPrefix": "¡Listo! Mi prefijo en tu servidor ahora es `{0}`. Intenta ejecutar `{1}ping` para probarlo.",
@@ -186,5 +188,5 @@
"invalidEndTime": "Tiempo de finalización inválido! El tiempo debe estar entre `00:00` y `{0}`.", "invalidEndTime": "Tiempo de finalización inválido! El tiempo debe estar entre `00:00` y `{0}`.",
"invalidTimeOrder": "El tiempo final no puede ser menor o igual que el tiempo de inicio.", "invalidTimeOrder": "El tiempo final no puede ser menor o igual que el tiempo de inicio.",
"SetStageAnnounceTemplate": "¡Hecho! A partir de ahora, el estado de voz como el que tienes ahora se nombrará según tu plantilla. Deberías verlo actualizarse en unos segundos." "setStageAnnounceTemplate": "¡Hecho! A partir de ahora, el estado de voz como el que tienes ahora se nombrará según tu plantilla. Deberías verlo actualizarse en unos segundos."
} }

View File

@@ -7,9 +7,11 @@
"noChannel": "接続する音声チャンネルがありません。提供するか、参加してください。", "noChannel": "接続する音声チャンネルがありません。提供するか、参加してください。",
"alreadyConnected": "すでに音声チャンネルに接続しています。", "alreadyConnected": "すでに音声チャンネルに接続しています。",
"noPermission": "申し訳ありません!私はあなたの音声チャンネルに参加または話すための許可がありません。", "noPermission": "申し訳ありません!私はあなたの音声チャンネルに参加または話すための許可がありません。",
"noCreatePermission": "ごめんなさい!曲リクエストチャンネルを作成する権限がありません。",
"noPlaySource": "再生可能なソースが見つかりません!", "noPlaySource": "再生可能なソースが見つかりません!",
"noPlayer": "このサーバーにプレイヤーが見つかりません。", "noPlayer": "このサーバーにプレイヤーが見つかりません。",
"notVote": "このコマンドにはあなたの投票が必要です!詳細については、/voteを入力してください。", "notVote": "このコマンドにはあなたの投票が必要です!詳細については、/voteを入力してください。",
"missingIntents": "申し訳ありませんが、このコマンドは実行できません。ボットに必要なリクエストインテントが不足しています:`({0})`.",
"languageNotFound": "言語パックが見つかりません。既存の言語パックを選択してください。", "languageNotFound": "言語パックが見つかりません。既存の言語パックを選択してください。",
"changedLanguage": "「{0}」言語パックに正常に変更しました。", "changedLanguage": "「{0}」言語パックに正常に変更しました。",
"setPrefix": "完了!あなたのサーバーのプレフィックスは今や「{0}」です。 `{1}ping`を実行してテストしてみてください。", "setPrefix": "完了!あなたのサーバーのプレフィックスは今や「{0}」です。 `{1}ping`を実行してテストしてみてください。",
@@ -186,5 +188,5 @@
"invalidEndTime": "無効な終了時間!時間は `00:00` と `{0}` の間に設定する必要があります。", "invalidEndTime": "無効な終了時間!時間は `00:00` と `{0}` の間に設定する必要があります。",
"invalidTimeOrder": "終了時間は開始時間より大きくない必要があります。", "invalidTimeOrder": "終了時間は開始時間より大きくない必要があります。",
"SetStageAnnounceTemplate": "完了!これからは、今いるボイスステータスがあなたのテンプレートに従って名前が付けられます。数秒以内に更新されるのを見ることができるはずです。" "setStageAnnounceTemplate": "完了!これからは、今いるボイスステータスがあなたのテンプレートに従って名前が付けられます。数秒以内に更新されるのを見ることができるはずです。"
} }

View File

@@ -7,9 +7,11 @@
"noChannel": "연결할 음성 채널이 없습니다. 하나를 제공하거나 참여하십시오.", "noChannel": "연결할 음성 채널이 없습니다. 하나를 제공하거나 참여하십시오.",
"alreadyConnected": "이미 음성 채널에 연결되어 있습니다.", "alreadyConnected": "이미 음성 채널에 연결되어 있습니다.",
"noPermission": "죄송합니다! 음성 채널에 참여하거나 말할 권한이 없습니다.", "noPermission": "죄송합니다! 음성 채널에 참여하거나 말할 권한이 없습니다.",
"noCreatePermission": "죄송합니다! 노래 요청 채널을 생성할 권한이 없습니다.",
"noPlaySource": "재생 가능한 소스를 찾을 수 없습니다!", "noPlaySource": "재생 가능한 소스를 찾을 수 없습니다!",
"noPlayer": "이 서버에서 플레이어를 찾을 수 없습니다.", "noPlayer": "이 서버에서 플레이어를 찾을 수 없습니다.",
"notVote": "이 명령어를 실행하려면 투표해야합니다! 자세한 내용은 `/vote`를 입력하십시오.", "notVote": "이 명령어를 실행하려면 투표해야합니다! 자세한 내용은 `/vote`를 입력하십시오.",
"missingIntents": "죄송하지만 이 명령을 실행할 수 없습니다. 봇에 필요한 요청 의도가 없습니다:`({0})`.",
"languageNotFound": "언어 팩을 찾을 수 없습니다! 기존 언어 팩을 선택하십시오.", "languageNotFound": "언어 팩을 찾을 수 없습니다! 기존 언어 팩을 선택하십시오.",
"changedLanguage": "성공적으로 `{0}` 언어 팩으로 변경되었습니다.", "changedLanguage": "성공적으로 `{0}` 언어 팩으로 변경되었습니다.",
"setPrefix": "완료되었습니다! 이 서버에서 내 접두사는 이제 `{0}`입니다. `{1}ping`을 실행하여 테스트해보세요.", "setPrefix": "완료되었습니다! 이 서버에서 내 접두사는 이제 `{0}`입니다. `{1}ping`을 실행하여 테스트해보세요.",
@@ -186,5 +188,5 @@
"invalidEndTime": "효력 없는 종료 시간! 시간은 `00:00` 과 `{0}` 사이에 설정해야 합니다.", "invalidEndTime": "효력 없는 종료 시간! 시간은 `00:00` 과 `{0}` 사이에 설정해야 합니다.",
"invalidTimeOrder": "종료 시간은 시작 시간보다 클수 있어야 합니다.", "invalidTimeOrder": "종료 시간은 시작 시간보다 클수 있어야 합니다.",
"SetStageAnnounceTemplate": "완료! 이제부터 지금 있는 음성 상태는 귀하의 템플릿에 따라 이름이 지정됩니다. 몇 초 후에 업데이트되는 것을 볼 수 있을 것입니다." "setStageAnnounceTemplate": "완료! 이제부터 지금 있는 음성 상태는 귀하의 템플릿에 따라 이름이 지정됩니다. 몇 초 후에 업데이트되는 것을 볼 수 있을 것입니다."
} }

View File

@@ -7,9 +7,11 @@
"noChannel": "Нет голосового канала для подключения. Пожалуйста, укажите или присоединитесь к одному.", "noChannel": "Нет голосового канала для подключения. Пожалуйста, укажите или присоединитесь к одному.",
"alreadyConnected": "Уже подключен к голосовому каналу.", "alreadyConnected": "Уже подключен к голосовому каналу.",
"noPermission": "Извините! У меня нет разрешения на подключение или разговор в вашем голосовом канале.", "noPermission": "Извините! У меня нет разрешения на подключение или разговор в вашем голосовом канале.",
"noCreatePermission": "Извините! У меня нет прав на создание канала для запроса песен.",
"noPlaySource": "Не получилось найти рабочие источники!", "noPlaySource": "Не получилось найти рабочие источники!",
"noPlayer": "На этом сервере не найдено ни одного активного плеера.", "noPlayer": "На этом сервере не найдено ни одного активного плеера.",
"notVote": "Эта команда требует вашего голоса! Введите `/vote` для получения дополнительной информации.", "notVote": "Эта команда требует вашего голоса! Введите `/vote` для получения дополнительной информации.",
"missingIntents": "Извините, но эту команду нельзя выполнить, так как у бота отсутствует необходимый запрос намерения: `({0})`.",
"languageNotFound": "Языковой пакет не найден! Пожалуйста, выберите существующий языковой пакет.", "languageNotFound": "Языковой пакет не найден! Пожалуйста, выберите существующий языковой пакет.",
"changedLanguage": "Язык успешно изменен на `{0}`.", "changedLanguage": "Язык успешно изменен на `{0}`.",
"setPrefix": "Готово! Мой префикс на вашем сервере теперь `{0}`. Попробуйте запустить `{1}ping`, чтобы проверить его.", "setPrefix": "Готово! Мой префикс на вашем сервере теперь `{0}`. Попробуйте запустить `{1}ping`, чтобы проверить его.",
@@ -185,5 +187,5 @@
"invalidEndTime": "Невозможное время конца! Вход времени должен быть внутри `00:00` и `{0}`.", "invalidEndTime": "Невозможное время конца! Вход времени должен быть внутри `00:00` и `{0}`.",
"invalidTimeOrder": "Время конца не может быть меньше или равно времени начала.", "invalidTimeOrder": "Время конца не может быть меньше или равно времени начала.",
"SetStageAnnounceTemplate": "Готово! С этого момента статус голоса, как тот, в котором вы находитесь сейчас, будет называться в соответствии с вашим шаблоном. Вы должны увидеть обновление через несколько секунд." "setStageAnnounceTemplate": "Готово! С этого момента статус голоса, как тот, в котором вы находитесь сейчас, будет называться в соответствии с вашим шаблоном. Вы должны увидеть обновление через несколько секунд."
} }

View File

@@ -7,9 +7,11 @@
"noChannel": "Немає голосового каналу для підключення. Будь ласка, вкажіть або приєднайтеся до одного.", "noChannel": "Немає голосового каналу для підключення. Будь ласка, вкажіть або приєднайтеся до одного.",
"alreadyConnected": "Уже підключений до голосового каналу.", "alreadyConnected": "Уже підключений до голосового каналу.",
"noPermission": "Вибачте! У мене немає дозволу на підключення або розмову у вашому голосовому каналі.", "noPermission": "Вибачте! У мене немає дозволу на підключення або розмову у вашому голосовому каналі.",
"noCreatePermission": "Вибачте! У мене немає прав для створення каналу запитів на пісні.",
"noPlaySource": "Неможливо знайти робочі джерела!", "noPlaySource": "Неможливо знайти робочі джерела!",
"noPlayer": "На цьому сервері не знайдено жодного активного плеєра.", "noPlayer": "На цьому сервері не знайдено жодного активного плеєра.",
"notVote": "Ця команда вимагає вашого голосу! Введіть `/vote` для отримання додаткової інформації.", "notVote": "Ця команда вимагає вашого голосу! Введіть `/vote` для отримання додаткової інформації.",
"missingIntents": "Вибачте, але цю команду не можна виконати, оскільки у бота відсутній необхідний запит на інтенцію: `({0})`.",
"languageNotFound": "Мовний пакет не знайдено! Будь ласка, виберіть наявний мовний пакет.", "languageNotFound": "Мовний пакет не знайдено! Будь ласка, виберіть наявний мовний пакет.",
"changedLanguage": "Успішно змінено на мовний пакет `{0}`.", "changedLanguage": "Успішно змінено на мовний пакет `{0}`.",
"setPrefix": "Готово! Мій префікс на вашому сервері тепер `{0}`. Спробуйте запустити `{1}ping`, щоб перевірити його.", "setPrefix": "Готово! Мій префікс на вашому сервері тепер `{0}`. Спробуйте запустити `{1}ping`, щоб перевірити його.",
@@ -185,5 +187,5 @@
"invalidEndTime": "Недійснений час закінчення! Час має бути в межах `00:00` та `{0}`.", "invalidEndTime": "Недійснений час закінчення! Час має бути в межах `00:00` та `{0}`.",
"invalidTimeOrder": "Час закінчення не може бути меншим або рівним часу початку.", "invalidTimeOrder": "Час закінчення не може бути меншим або рівним часу початку.",
"SetStageAnnounceTemplate": "Готово! Відтепер статус голосу, як той, в якому ви зараз перебуваєте, буде називатися відповідно до вашого шаблону. Ви повинні побачити оновлення через кілька секунд." "setStageAnnounceTemplate": "Готово! Відтепер статус голосу, як той, в якому ви зараз перебуваєте, буде називатися відповідно до вашого шаблону. Ви повинні побачити оновлення через кілька секунд."
} }

View File

@@ -69,10 +69,10 @@
"Remove tracks requested by a specific member.": "刪除指定成員所要求的歌曲。", "Remove tracks requested by a specific member.": "刪除指定成員所要求的歌曲。",
"forward": "前進", "forward": "前進",
"Forwards by a certain amount of time in the current track. The default is 10 seconds.": "在目前歌曲中前進一定的時間。預設為 10 秒。", "Forwards by a certain amount of time in the current track. The default is 10 seconds.": "在目前歌曲中前進一定的時間。預設為 10 秒。",
"Input a amount that you to forward to. Exmaple: 1: 20": "輸入您要前進到的時間。範例1:20", "Input an amount that you to forward to. Exmaple: 1:20": "輸入您要前進到的時間。範例1:20",
"rewind": "倒退", "rewind": "倒退",
"Rewind by a certain amount of time in the current track. The default is 10 seconds.": "在目前歌曲中倒退一定的時間。預設為 10 秒。", "Rewind by a certain amount of time in the current track. The default is 10 seconds.": "在目前歌曲中倒退一定的時間。預設為 10 秒。",
"Input a amount that you to rewind to. Exmaple: 1: 20": "輸入您要倒退到的時間。範例1:20", "Input an amount that you to rewind to. Exmaple: 1:20": "輸入您要倒退到的時間。範例1:20",
"replay": "重新播放", "replay": "重新播放",
"Reset the progress of the current song.": "重設目前歌曲的進度。", "Reset the progress of the current song.": "重設目前歌曲的進度。",
"shuffle": "隨機播放", "shuffle": "隨機播放",
@@ -210,5 +210,22 @@
"cleareffect": "清除效果", "cleareffect": "清除效果",
"Clear all or specific sound effects.": "清除所有或指定的音效。", "Clear all or specific sound effects.": "清除所有或指定的音效。",
"effect": "效果", "effect": "效果",
"Remove a specific sound effects.": "刪除指定的音效。" "Remove a specific sound effects.": "刪除指定的音效。",
"start": "開始",
"end": "結束",
"Specify a time you would like to start, e.g. 1:00": "指定您希望開始的時間例如1:00。",
"Specify a time you would like to end, e.g. 4:00": "指定您希望結束的時間例如4:00。",
"list": "列表",
"Customize the channel topic template": "自訂頻道主題模板",
"template": "模板",
"setupchannel": "設置頻道",
"Sets up a dedicated channel for song requests in your server.": "為您的伺服器設置一個專用的歌曲請求頻道。",
"Provide a request channel. If not, a text channel will be generated.": "提供請求頻道。如果沒有,將生成一個文本頻道。",
"ping": "ping",
"…": "...",
"8d": "8d",
"dj": "dj",
"247": "247",
"stageannounce": "舞台公告",
"Soundcloud": "Soundcloud"
} }

58
main.py
View File

@@ -21,8 +21,18 @@ class Translator(discord.app_commands.Translator):
func.logger.info("Unload Translator") func.logger.info("Unload Translator")
async def translate(self, string: discord.app_commands.locale_str, locale: discord.Locale, context: discord.app_commands.TranslationContext): async def translate(self, string: discord.app_commands.locale_str, locale: discord.Locale, context: discord.app_commands.TranslationContext):
if str(locale) in func.LOCAL_LANGS: locale_key = str(locale)
return func.LOCAL_LANGS[str(locale)].get(string.message, None)
if locale_key in func.LOCAL_LANGS:
translated_text = func.LOCAL_LANGS[locale_key].get(string.message)
if translated_text is None:
missing_translations = func.MISSING_TRANSLATOR.setdefault(locale_key, [])
if string.message not in missing_translations:
missing_translations.append(string.message)
return translated_text
return None return None
class Vocard(commands.Bot): class Vocard(commands.Bot):
@@ -32,15 +42,37 @@ class Vocard(commands.Bot):
self.ipc: IPCClient self.ipc: IPCClient
async def on_message(self, message: discord.Message, /) -> None: async def on_message(self, message: discord.Message, /) -> None:
# Ignore messages from bots or DMs
if message.author.bot or not message.guild: if message.author.bot or not message.guild:
return False return False
# Check if the bot is directly mentioned
if self.user.id in message.raw_mentions and not message.mention_everyone: if self.user.id in message.raw_mentions and not message.mention_everyone:
prefix = await self.command_prefix(self, message) prefix = await self.command_prefix(self, message)
if not prefix: if not prefix:
return await message.channel.send("I don't have a bot prefix set.") return await message.channel.send("I don't have a bot prefix set.")
await message.channel.send(f"My prefix is `{prefix}`") await message.channel.send(f"My prefix is `{prefix}`")
# Fetch guild settings and check if the mesage is in the music request channel
settings = await func.get_settings(message.guild.id)
if settings and (request_channel := settings.get("music_request_channel")):
if message.channel.id == request_channel.get("text_channel_id"):
ctx = await self.get_context(message)
try:
cmd = self.get_command("play")
if message.content:
await cmd(ctx, query=message.content)
elif message.attachments:
for attachment in message.attachments:
await cmd(ctx, query=attachment.url)
except Exception as e:
await func.send(ctx, str(e), ephemeral=True)
finally:
return await message.delete()
await self.process_commands(message) await self.process_commands(message)
async def connect_db(self) -> None: async def connect_db(self) -> None:
@@ -65,6 +97,9 @@ class Vocard(commands.Bot):
# Connecting to MongoDB # Connecting to MongoDB
await self.connect_db() await self.connect_db()
# Set translator
await self.tree.set_translator(Translator())
# Loading all the module in `cogs` folder # Loading all the module in `cogs` folder
for module in os.listdir(func.ROOT_DIR + '/cogs'): for module in os.listdir(func.ROOT_DIR + '/cogs'):
if module.endswith('.py'): if module.endswith('.py'):
@@ -82,10 +117,10 @@ class Vocard(commands.Bot):
func.logger.error(f"Cannot connected to dashboard! - Reason: {e}") func.logger.error(f"Cannot connected to dashboard! - Reason: {e}")
if not func.settings.version or func.settings.version != update.__version__: if not func.settings.version or func.settings.version != update.__version__:
func.update_json("settings.json", new_data={"version": update.__version__})
await self.tree.set_translator(Translator())
await self.tree.sync() await self.tree.sync()
func.update_json("settings.json", new_data={"version": update.__version__})
for locale_key, values in func.MISSING_TRANSLATOR.items():
func.logger.warning(f"Missing translation for '{", ".join(values)}' in '{locale_key}'")
async def on_ready(self): async def on_ready(self):
func.logger.info("------------------") func.logger.info("------------------")
@@ -98,6 +133,7 @@ class Vocard(commands.Bot):
func.settings.client_id = self.user.id func.settings.client_id = self.user.id
func.LOCAL_LANGS.clear() func.LOCAL_LANGS.clear()
func.MISSING_TRANSLATOR.clear()
async def on_command_error(self, ctx: commands.Context, exception, /) -> None: async def on_command_error(self, ctx: commands.Context, exception, /) -> None:
error = getattr(exception, 'original', exception) error = getattr(exception, 'original', exception)
@@ -139,9 +175,15 @@ class CommandCheck(discord.app_commands.CommandTree):
return True return True
async def get_prefix(bot, message: discord.Message): async def get_prefix(bot: commands.Bot, message: discord.Message) -> str:
settings = await func.get_settings(message.guild.id) settings = await func.get_settings(message.guild.id)
return settings.get("prefix", func.settings.bot_prefix) prefix = settings.get("prefix", func.settings.bot_prefix)
# Allow owner to use the bot without a prefix
if prefix and not message.content.startswith(prefix) and (await bot.is_owner(message.author) or message.author.id in func.settings.bot_access_user):
return ""
return prefix
# Loading settings and logger # Loading settings and logger
func.settings = Settings(func.open_json("settings.json")) func.settings = Settings(func.open_json("settings.json"))
@@ -164,7 +206,7 @@ if (LOG_FILE := LOG_SETTINGS.get("file", {})).get("enable", True):
# Setup the bot object # Setup the bot object
intents = discord.Intents.default() intents = discord.Intents.default()
intents.message_content = True if func.settings.bot_prefix else False intents.message_content = False if func.settings.bot_prefix is None else True
intents.members = func.settings.ipc_client.get("enable", False) intents.members = func.settings.ipc_client.get("enable", False)
intents.voice_states = True intents.voice_states = True

View File

@@ -5,4 +5,5 @@ tldextract==3.2.1
validators==0.18.2 validators==0.18.2
humanize==4.0.0 humanize==4.0.0
beautifulsoup4==4.11.1 beautifulsoup4==4.11.1
psutil==5.9.8 psutil==5.9.8
aiohttp==3.9.5

View File

@@ -82,6 +82,10 @@
"tiktok": { "tiktok": {
"emoji": "<:tiktok:996007689798811698>", "emoji": "<:tiktok:996007689798811698>",
"color": "0x74ECE9" "color": "0x74ECE9"
},
"others": {
"emoji": "🌎",
"color": "0xb3b3b3"
} }
}, },
"default_controller": { "default_controller": {

View File

@@ -2,7 +2,7 @@ import requests, zipfile, os, shutil, argparse
from io import BytesIO from io import BytesIO
ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
__version__ = "v2.6.9" __version__ = "v2.7.0b3"
GITHUB_API_URL = "https://api.github.com/repos/ChocoMeow/Vocard/releases/latest" GITHUB_API_URL = "https://api.github.com/repos/ChocoMeow/Vocard/releases/latest"
VOCARD_URL = "https://github.com/ChocoMeow/Vocard/archive/" VOCARD_URL = "https://github.com/ChocoMeow/Vocard/archive/"

View File

@@ -45,11 +45,11 @@ class ControlButton(discord.ui.Button):
self.disable_button_text: bool = func.settings.controller.get("disableButtonText", False) self.disable_button_text: bool = func.settings.controller.get("disableButtonText", False)
super().__init__(label=self.player.get_msg(label) if label and not self.disable_button_text else None, **kwargs) super().__init__(label=self.player.get_msg(label) if label and not self.disable_button_text else None, **kwargs)
async def send(self, interaction: discord.Interaction, key:str, *params, ephemeral: bool = False) -> None: async def send(self, interaction: discord.Interaction, key: str, *params, ephemeral: bool = False) -> None:
stay = self.player.settings.get("controller_msg", True) stay = self.player.settings.get("controller_msg", True)
return await func.send( return await func.send(
interaction, key, *params, interaction, key, *params,
delete_after=None if ephemeral or stay is True else 10, delete_after=None if ephemeral or stay else 10,
ephemeral=ephemeral ephemeral=ephemeral
) )
@@ -82,8 +82,8 @@ class Back(ControlButton):
await self.send(interaction, "backed", interaction.user) await self.send(interaction, "backed", interaction.user)
if self.player.queue._repeat.mode == voicelink.LoopType.track: if self.player.queue._repeat.mode == voicelink.LoopType.TRACK:
await self.player.set_repeat(voicelink.LoopType.off.name) await self.player.set_repeat(voicelink.LoopType.OFF)
class Resume(ControlButton): class Resume(ControlButton):
def __init__(self, **kwargs): def __init__(self, **kwargs):
@@ -140,8 +140,8 @@ class Skip(ControlButton):
await self.send(interaction, "skipped", interaction.user) await self.send(interaction, "skipped", interaction.user)
if self.player.queue._repeat.mode == voicelink.LoopType.track: if self.player.queue._repeat.mode == voicelink.LoopType.TRACK:
await self.player.set_repeat(voicelink.LoopType.off.name) await self.player.set_repeat(voicelink.LoopType.OFF)
await self.player.stop() await self.player.stop()
class Stop(ControlButton): class Stop(ControlButton):
@@ -222,7 +222,7 @@ class Loop(ControlButton):
self.emoji = self.get_next_loop_emoji(self.player) self.emoji = self.get_next_loop_emoji(self.player)
await interaction.response.edit_message(view=self.view) await interaction.response.edit_message(view=self.view)
await self.send(interaction, 'repeat', mode.capitalize()) await self.send(interaction, 'repeat', mode.name.capitalize())
class VolumeUp(ControlButton): class VolumeUp(ControlButton):
def __init__(self, **kwargs): def __init__(self, **kwargs):

View File

@@ -24,7 +24,7 @@ SOFTWARE.
__version__ = "1.4" __version__ = "1.4"
__author__ = 'Vocard Development, Choco' __author__ = 'Vocard Development, Choco'
__license__ = "MIT" __license__ = "MIT"
__copyright__ = "Copyright 2023 (c) Vocard Development, Choco" __copyright__ = "Copyright 2023 - present (c) Vocard Development, Choco"
from .enums import SearchType, LoopType from .enums import SearchType, LoopType
from .events import * from .events import *

View File

@@ -26,35 +26,42 @@ from enum import Enum, auto
class LoopType(Enum): class LoopType(Enum):
"""The enum for the different loop types for Voicelink """The enum for the different loop types for Voicelink
LoopType.off: 1 LoopType.OFF: 1
LoopType.track: 2 LoopType.TRACK: 2
LoopType.queue: 3 LoopType.QUEUE: 3
""" """
off = auto() OFF = auto()
track = auto() TRACK = auto()
queue = auto() QUEUE = auto()
class SearchType(Enum): class SearchType(Enum):
"""The enum for the different search types for Voicelink. """The enum for the different search types for Voicelink.
This feature is exclusively for the Spotify search feature of Voicelink. This feature is exclusively for the Spotify search feature of Voicelink.
If you are not using this feature, this class is not necessary. If you are not using this feature, this class is not necessary.
SearchType.ytsearch searches using regular Youtube, SearchType.YOUTUBE searches using regular Youtube,
which is best for all scenarios. which is best for all scenarios.
SearchType.ytmsearch searches using YouTube Music, SearchType.YOUTUBE_MUSIC searches using YouTube Music,
which is best for getting audio-only results. which is best for getting audio-only results.
SearchType.SPOTIFY searches using Spotify,
which is an alternative to YouTube or YouTube Music.
SearchType.scsearch searches using SoundCloud, SearchType.SOUNDCLOUD searches using SoundCloud,
which is an alternative to YouTube or YouTube Music.
SearchType.APPLE_MUSIC searches using Apple Music,
which is an alternative to YouTube or YouTube Music. which is an alternative to YouTube or YouTube Music.
""" """
ytsearch = "ytsearch" YOUTUBE = "ytsearch"
ytmsearch = "ytmsearch" YOUTUBE_MUSIC = "ytmsearch"
scsearch = "scsearch" SPOTIFY = "spsearch"
amsearch = "amsearch" SOUNDCLOUD = "scsearch"
APPLE_MUSIC = "amsearch"
def __str__(self) -> str: def __str__(self) -> str:
return self.value return self.value
@@ -62,10 +69,10 @@ class SearchType(Enum):
class RequestMethod(Enum): class RequestMethod(Enum):
"""The enum for the different request methods in Voicelink """The enum for the different request methods in Voicelink
""" """
get = "get" GET = "get"
patch = "patch" PATCH = "patch"
delete = "delete" DELETE = "delete"
post = "post" POST = "post"
def __str__(self) -> str: def __str__(self) -> str:
return self.value return self.value
@@ -86,9 +93,9 @@ class NodeAlgorithm(Enum):
""" """
# We don't have to define anything special for these, since these just serve as flags # We don't have to define anything special for these, since these just serve as flags
by_ping = auto() BY_PING = auto()
by_region = auto() BY_REGION = auto()
by_players = auto() BY_PLAYERS = auto()
def __str__(self) -> str: def __str__(self) -> str:
return self.value return self.value

View File

@@ -72,7 +72,7 @@ class Track:
track_id: str = None, track_id: str = None,
info: dict, info: dict,
requester: Member, requester: Member,
search_type: SearchType = SearchType.ytsearch, search_type: SearchType = SearchType.YOUTUBE,
spotify_track = None, spotify_track = None,
): ):
self._track_id: Optional[str] = track_id self._track_id: Optional[str] = track_id
@@ -88,7 +88,7 @@ class Track:
self.artist_id: Optional[list] = info.get("artist_id") self.artist_id: Optional[list] = info.get("artist_id")
self.original: Optional[Track] = None if self.spotify else self self.original: Optional[Track] = None if self.spotify else self
self._search_type: SearchType = SearchType.ytsearch if self.spotify else search_type self._search_type: SearchType = SearchType.YOUTUBE if self.spotify else search_type
self.spotify_track: Track = spotify_track self.spotify_track: Track = spotify_track
self.thumbnail: str = info.get("artworkUrl") self.thumbnail: str = info.get("artworkUrl")

View File

@@ -34,7 +34,7 @@ class Placeholders:
"track_color": self.track_color, "track_color": self.track_color,
"track_requester_id": self.track_requester_id, "track_requester_id": self.track_requester_id,
"track_requester_name": self.track_requester_name, "track_requester_name": self.track_requester_name,
"track_requester_metion": self.track_requester_mention, "track_requester_mention": self.track_requester_mention,
"track_requester_avatar": self.track_requester_avatar, "track_requester_avatar": self.track_requester_avatar,
"track_source_name": self.track_source_name, "track_source_name": self.track_source_name,
"track_source_emoji": self.track_source_emoji, "track_source_emoji": self.track_source_emoji,

View File

@@ -36,6 +36,7 @@ from discord import (
VoiceProtocol, VoiceProtocol,
Member, Member,
Message, Message,
PartialMessage,
Interaction, Interaction,
errors errors
) )
@@ -112,7 +113,7 @@ class Player(VoiceProtocol):
self.queue: Queue = eval(self.settings.get("queueType", "Queue"))(self.settings.get("maxQueue", func.settings.max_queue), self.settings.get("duplicateTrack", True), self.get_msg) self.queue: Queue = eval(self.settings.get("queueType", "Queue"))(self.settings.get("maxQueue", func.settings.max_queue), self.settings.get("duplicateTrack", True), self.get_msg)
self._node = NodePool.get_node() self._node = NodePool.get_node()
self._current: Track = None self._current: Optional[Track] = None
self._filters: Filters = Filters() self._filters: Filters = Filters()
self._paused: bool = False self._paused: bool = False
self._is_connected: bool = False self._is_connected: bool = False
@@ -126,8 +127,8 @@ class Player(VoiceProtocol):
self._voice_state: dict = {} self._voice_state: dict = {}
self.controller: Message = None self.controller: Union[Message, PartialMessage] = None
self.updating: bool = False self._updating: bool = False
self.pause_votes = set() self.pause_votes = set()
self.resume_votes = set() self.resume_votes = set()
@@ -180,7 +181,7 @@ class Player(VoiceProtocol):
return self._is_connected and self._paused return self._is_connected and self._paused
@property @property
def current(self) -> Track: def current(self) -> Optional[Track]:
"""Property which returns the currently playing track""" """Property which returns the currently playing track"""
return self._current return self._current
@@ -218,12 +219,26 @@ class Player(VoiceProtocol):
@property @property
def ping(self) -> float: def ping(self) -> float:
"""Calculates and returns the player's current ping in seconds."""
return round(self._ping / 1000, 2) return round(self._ping / 1000, 2)
@property
def is_ipc_connected(self) -> bool:
"""Indicates whether the Inter-Process Communication (IPC) connection is active."""
return self._ipc._is_connected and self._ipc_connection
def get_msg(self, *keys) -> Union[list[str], str]: def get_msg(self, *keys) -> Union[list[str], str]:
"""Retrieves a localized message or list of messages based on the given keys
for the guild associated with this player.
"""
return func.get_lang_non_async(self.guild.id, *keys) return func.get_lang_non_async(self.guild.id, *keys)
def required(self, leave=False): def required(self, leave=False):
"""
Calculates the number of votes required for a specific action in the voice channel.
If `leave` is True and the channel has three members, the requirement adjusts to 2 votes.
"""
if self.settings.get('votedisable'): if self.settings.get('votedisable'):
return 0 return 0
@@ -233,18 +248,22 @@ class Player(VoiceProtocol):
required = 2 required = 2
return required return required
@property
def is_ipc_connected(self) -> bool:
return self._ipc._is_connected and self._ipc_connection
def is_user_join(self, user: Member): def is_user_join(self, user: Member):
"""Checks if a user is present in the voice channel or has 'Manage Server' permission."""
if user not in self.channel.members: if user not in self.channel.members:
if not user.guild_permissions.manage_guild: if not user.guild_permissions.manage_guild:
return False return False
return True return True
def is_privileged(self, user: Member, check_user_join: bool = True) -> bool: def is_privileged(self, user: Member, check_user_join: bool = True) -> bool:
"""
Determines if a user has privileged access.
Privileged access is granted if the user is in the bot access list,
has 'Manage Server' permission, or meets the DJ role criteria in the settings.
Raises an exception if `check_user_join` is True and the user is not in the channel.
"""
if user.id in func.settings.bot_access_user: if user.id in func.settings.bot_access_user:
return True return True
@@ -256,11 +275,20 @@ class Player(VoiceProtocol):
return manage_perm or (self.settings['dj'] in [role.id for role in user.roles]) return manage_perm or (self.settings['dj'] in [role.id for role in user.roles])
return self.dj.id == user.id or manage_perm return self.dj.id == user.id or manage_perm
def build_embed(self, current_track: Track = None):
"""Builds an embed based on the current track state."""
controller = self.settings.get("default_controller", func.settings.controller).get("embeds", {})
raw = controller.get("active" if current_track else "inactive", {})
return build_embed(raw, self._ph)
async def send(self, method: RequestMethod, query: str = None, data: Union[Dict, str] = {}) -> Dict: async def send(self, method: RequestMethod, query: str = None, data: Union[Dict, str] = {}) -> Dict:
"""Sends an HTTP request to the node with the given method, query, and data."""
uri: str = f"sessions/{self._node._session_id}/players/{self._guild.id}" + (f"?{query}" if query else "") uri: str = f"sessions/{self._node._session_id}/players/{self._guild.id}" + (f"?{query}" if query else "")
return await self._node.send(method, query=uri, data=data) return await self._node.send(method, query=uri, data=data)
async def _update_state(self, data: dict) -> None: async def _update_state(self, data: dict) -> None:
"""Updates the player's state based on the provided data."""
state: dict = data.get("state") state: dict = data.get("state")
self._last_update = time.time() * 1000 self._last_update = time.time() * 1000
self._is_connected = state.get("connected") self._is_connected = state.get("connected")
@@ -277,6 +305,7 @@ class Player(VoiceProtocol):
}) })
async def _dispatch_voice_update(self, voice_data: Dict[str, Any] = None): async def _dispatch_voice_update(self, voice_data: Dict[str, Any] = None):
"""Dispatches a voice update to the node."""
if {"sessionId", "event"} != self._voice_state.keys(): if {"sessionId", "event"} != self._voice_state.keys():
self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) dispatched voice update failed {voice_data}") self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) dispatched voice update failed {voice_data}")
return return
@@ -289,14 +318,16 @@ class Player(VoiceProtocol):
"sessionId": state['sessionId'], "sessionId": state['sessionId'],
} }
await self.send(method=RequestMethod.patch, data={"voice": data}) await self.send(method=RequestMethod.PATCH, data={"voice": data})
self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) dispatched voice update to {state['event']['endpoint']} with data {data}") self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) dispatched voice update to {state['event']['endpoint']} with data {data}")
async def on_voice_server_update(self, data: dict): async def on_voice_server_update(self, data: dict):
"""Handles a voice server update event."""
self._voice_state.update({"event": data}) self._voice_state.update({"event": data})
await self._dispatch_voice_update(self._voice_state) await self._dispatch_voice_update(self._voice_state)
async def on_voice_state_update(self, data: dict): async def on_voice_state_update(self, data: dict):
"""Handles a voice state update event."""
self._voice_state.update({"sessionId": data.get("session_id")}) self._voice_state.update({"sessionId": data.get("session_id")})
if not (channel_id := data.get("channel_id")): if not (channel_id := data.get("channel_id")):
@@ -312,6 +343,7 @@ class Player(VoiceProtocol):
await self._dispatch_voice_update({**self._voice_state, "event": data}) await self._dispatch_voice_update({**self._voice_state, "event": data})
async def _dispatch_event(self, data: dict): async def _dispatch_event(self, data: dict):
"""Dispatches an event based on the type of event data received."""
event_type = data.get("type") event_type = data.get("type")
event: VoicelinkEvent = getattr(events, event_type)(data, self) event: VoicelinkEvent = getattr(events, event_type)(data, self)
@@ -326,6 +358,7 @@ class Player(VoiceProtocol):
self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) dispatched event {event_type}.") self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) dispatched event {event_type}.")
async def do_next(self): async def do_next(self):
"""Processes the next track in the queue."""
if self._current or self.is_playing or not self.channel: if self._current or self.is_playing or not self.channel:
return return
@@ -378,42 +411,48 @@ class Player(VoiceProtocol):
}) })
async def invoke_controller(self): async def invoke_controller(self):
if self.updating or not self.channel: """Sends or updates the music controller message in the designated channel."""
if self._updating or not self.channel:
return return
self.updating = True self._updating = True
try: try:
embed, view = await self.build_embed(), InteractiveController(self) embed, view = self.build_embed(self.current), InteractiveController(self)
if not self.controller: if not self.controller:
self.controller = await self.context.channel.send(embed=embed, view=view) if request_channel_data := self.settings.get("music_request_channel"):
channel = self.bot.get_channel(request_channel_data.get("text_channel_id"))
if channel:
self.controller = channel.get_partial_message(request_channel_data.get("controller_msg_id"))
try:
await self.controller.edit(embed=embed, view=view)
except errors.NotFound:
self.controller = None
# Send a new controller message if none exists
if not self.controller:
self.controller = await self.context.channel.send(embed=embed, view=view)
elif not await self.is_position_fresh(): elif not await self.is_position_fresh():
try: try:
await self.controller.delete() await self.controller.delete()
except: except Exception as e:
pass self._logger.warning(
f"Failed to delete outdated controller in {self.guild.name}({self.guild.id}): {e}"
)
self.controller = await self.context.channel.send(embed=embed, view=view) self.controller = await self.context.channel.send(embed=embed, view=view)
else: else:
await self.controller.edit(embed=embed, view=view) await self.controller.edit(embed=embed, view=view)
except errors.Forbidden:
pass
except Exception as e: except Exception as e:
self._logger.error(f"Something went wrong while sending music controller to {self.guild.name}({self.guild.id})", exc_info=e) self._logger.error(f"Something went wrong while sending music controller to {self.guild.name}({self.guild.id})", exc_info=e)
pass
self.updating = False
async def build_embed(self):
controller = self.settings.get("default_controller", func.settings.controller).get("embeds", {})
raw = controller.get("active" if self.current else "inactive", {})
return build_embed(raw, self._ph) finally:
self._updating = False
async def is_position_fresh(self): async def is_position_fresh(self):
"""Checks if the current controller message is among the most recent messages."""
try: try:
async for message in self.context.channel.history(limit=5): async for message in self.context.channel.history(limit=5):
if message.id == self.controller.id: if message.id == self.controller.id:
@@ -424,19 +463,24 @@ class Player(VoiceProtocol):
return False return False
async def teardown(self): async def teardown(self):
await func.update_settings( """Cleans up the player and associated resources."""
self.guild.id, try:
{"$set": { await func.update_settings(self.guild.id, {"$set": {
"lastActice": (timeNow := round(time.time())), "lastActice": (timeNow := round(time.time())),
"playTime": round(self.settings.get("playTime", 0) + ((timeNow - self.joinTime) / 60), 2) "playTime": round(self.settings.get("playTime", 0) + ((timeNow - self.joinTime) / 60), 2)
}} }})
)
await self.update_voice_status(remove_status=True) if self.is_ipc_connected:
if self.is_ipc_connected: await self.send_ws({"op": "playerClose"})
await self.send_ws({"op": "playerClose"}) except:
pass
try: try:
await self.controller.delete() await self.update_voice_status(remove_status=True)
if self.controller and self.controller.id == self.settings.get("music_request_channel", {}).get("controller_msg_id"):
await self.controller.edit(embed=self.build_embed(), view=None)
else:
await self.controller.delete()
except: except:
pass pass
@@ -450,7 +494,7 @@ class Player(VoiceProtocol):
query: str, query: str,
*, *,
requester: Member, requester: Member,
search_type: SearchType = SearchType.ytsearch search_type: SearchType = SearchType.YOUTUBE
) -> Union[List[Track], Playlist]: ) -> Union[List[Track], Playlist]:
"""Fetches tracks from the node's REST api to parse into Lavalink. """Fetches tracks from the node's REST api to parse into Lavalink.
@@ -464,6 +508,7 @@ class Player(VoiceProtocol):
return await self._node.get_tracks(query, requester=requester, search_type=search_type) return await self._node.get_tracks(query, requester=requester, search_type=search_type)
async def connect(self, *, timeout: float, reconnect: bool, self_deaf: bool = True, self_mute: bool = False): async def connect(self, *, timeout: float, reconnect: bool, self_deaf: bool = True, self_mute: bool = False):
"""Connects the player to a voice channel."""
await self.guild.change_voice_state(channel=self.channel, self_deaf=True, self_mute=self_mute) await self.guild.change_voice_state(channel=self.channel, self_deaf=True, self_mute=self_mute)
self._node._players[self.guild.id] = self self._node._players[self.guild.id] = self
self._is_connected = True self._is_connected = True
@@ -474,7 +519,7 @@ class Player(VoiceProtocol):
async def stop(self): async def stop(self):
"""Stops the currently playing track.""" """Stops the currently playing track."""
self._current = None self._current = None
await self.send(method=RequestMethod.patch, data={'encodedTrack': None}) await self.send(method=RequestMethod.PATCH, data={'encodedTrack': None})
async def disconnect(self, *, force: bool = False): async def disconnect(self, *, force: bool = False):
"""Disconnects the player from voice.""" """Disconnects the player from voice."""
@@ -498,7 +543,7 @@ class Player(VoiceProtocol):
assert self.channel is None and not self.is_connected assert self.channel is None and not self.is_connected
self._node._players.pop(self.guild.id) self._node._players.pop(self.guild.id)
await self.send(method=RequestMethod.delete) await self.send(method=RequestMethod.DELETE)
async def play( async def play(
self, self,
@@ -514,7 +559,7 @@ class Player(VoiceProtocol):
if track.spotify: if track.spotify:
if not track.original: if not track.original:
search_results = await self._node.get_tracks(f"ytmsearch:{track.author} - {track.title}", requester=track.requester) search_results = await self._node.get_tracks(f"{track.author} - {track.title}", requester=track.requester)
if not search_results: if not search_results:
raise TrackLoadError("Can't find a playable source!") raise TrackLoadError("Can't find a playable source!")
track.original = search_results[0] track.original = search_results[0]
@@ -527,7 +572,7 @@ class Player(VoiceProtocol):
if end or track.end_time: if end or track.end_time:
data["endTime"] = str(end if end else track.end_time) data["endTime"] = str(end if end else track.end_time)
await self.send(method=RequestMethod.patch, query=f"noReplace={ignore_if_playing}", data=data) await self.send(method=RequestMethod.PATCH, query=f"noReplace={ignore_if_playing}", data=data)
self._current = track self._current = track
@@ -538,6 +583,7 @@ class Player(VoiceProtocol):
return self._current return self._current
def _validate_time(self, track: Track, start_time: int, end_time: int) -> None: def _validate_time(self, track: Track, start_time: int, end_time: int) -> None:
"""Validates the start and end times for a track."""
if start_time or end_time: if start_time or end_time:
if not end_time: if not end_time:
end_time = track.length end_time = track.length
@@ -555,6 +601,7 @@ class Player(VoiceProtocol):
track.end_time = end_time track.end_time = end_time
async def add_track(self, raw_tracks: Union[Track, List[Track]], *, start_time: int = 0, end_time: int = 0, at_front: bool = False, duplicate: bool = True) -> int: async def add_track(self, raw_tracks: Union[Track, List[Track]], *, start_time: int = 0, end_time: int = 0, at_front: bool = False, duplicate: bool = True) -> int:
"""Adds one or more tracks to the queue."""
tracks: List[Track] = [] tracks: List[Track] = []
_duplicate_tracks = [] if self.queue._allow_duplicate and duplicate else [track.uri for track in self.queue._queue] _duplicate_tracks = [] if self.queue._allow_duplicate and duplicate else [track.uri for track in self.queue._queue]
raw_tracks = raw_tracks[0] if isinstance(raw_tracks, List) and len(raw_tracks) == 1 else raw_tracks raw_tracks = raw_tracks[0] if isinstance(raw_tracks, List) and len(raw_tracks) == 1 else raw_tracks
@@ -586,6 +633,7 @@ class Player(VoiceProtocol):
return len(tracks) if is_list else position return len(tracks) if is_list else position
async def remove_track(self, index: int, index2: int = None, remove_target: Member = None, requester: Member = None) -> Dict[int, Track]: async def remove_track(self, index: int, index2: int = None, remove_target: Member = None, requester: Member = None) -> Dict[int, Track]:
"""Removes one or more tracks from the queue."""
removed_tracks = self.queue.remove(index, index2, remove_target) removed_tracks = self.queue.remove(index, index2, remove_target)
if removed_tracks and self.is_ipc_connected: if removed_tracks and self.is_ipc_connected:
await self.send_ws({ await self.send_ws({
@@ -601,7 +649,7 @@ class Player(VoiceProtocol):
if position < 0 or position > self._current.original.length: if position < 0 or position > self._current.original.length:
raise TrackInvalidPosition("Seek position must be between 0 and the track length") raise TrackInvalidPosition("Seek position must be between 0 and the track length")
await self.send(method=RequestMethod.patch, data={"position": position}) await self.send(method=RequestMethod.PATCH, data={"position": position})
if self.is_ipc_connected: if self.is_ipc_connected:
await self.send_ws({"op": "updatePosition", "position": position}, requester) await self.send_ws({"op": "updatePosition", "position": position}, requester)
@@ -613,7 +661,7 @@ class Player(VoiceProtocol):
self._paused = pause self._paused = pause
self.pause_votes.clear() if pause else self.resume_votes.clear() self.pause_votes.clear() if pause else self.resume_votes.clear()
await self.send(method=RequestMethod.patch, data={"paused": pause}) await self.send(method=RequestMethod.PATCH, data={"paused": pause})
if self.is_ipc_connected: if self.is_ipc_connected:
await self.send_ws({"op": "updatePause", "pause": pause}, requester) await self.send_ws({"op": "updatePause", "pause": pause}, requester)
@@ -623,7 +671,7 @@ class Player(VoiceProtocol):
async def set_volume(self, volume: int, requester: Member = None) -> int: async def set_volume(self, volume: int, requester: Member = None) -> int:
"""Sets the volume of the player as an integer. Lavalink accepts values from 0 to 500.""" """Sets the volume of the player as an integer. Lavalink accepts values from 0 to 500."""
await self.send(method=RequestMethod.patch, data={"volume": volume}) await self.send(method=RequestMethod.PATCH, data={"volume": volume})
self._volume = volume self._volume = volume
if self.is_ipc_connected: if self.is_ipc_connected:
@@ -651,6 +699,7 @@ class Player(VoiceProtocol):
self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) has been shuffled the queue.") self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) has been shuffled the queue.")
async def swap_track(self, index1: int, index2: int, requester: Member = None) -> Tuple[Track, Track]: async def swap_track(self, index1: int, index2: int, requester: Member = None) -> Tuple[Track, Track]:
"""Swaps two tracks in the queue at the specified indices."""
track1, track2 = self.queue.swap(index1, index2) track1, track2 = self.queue.swap(index1, index2)
if self.is_ipc_connected: if self.is_ipc_connected:
await self.send_ws({ await self.send_ws({
@@ -661,6 +710,7 @@ class Player(VoiceProtocol):
return track1, track2 return track1, track2
async def move_track(self, index: int, new_index: int, requester: Member = None) -> Optional[Track]: async def move_track(self, index: int, new_index: int, requester: Member = None) -> Optional[Track]:
"""Moves a track from its current position to a new position in the queue."""
moved_track = self.queue.move(index, new_index) moved_track = self.queue.move(index, new_index)
if self.is_ipc_connected: if self.is_ipc_connected:
@@ -668,34 +718,31 @@ class Player(VoiceProtocol):
return moved_track return moved_track
async def set_repeat(self, mode: str = None, requester: Member = None) -> str: async def set_repeat(self, mode: LoopType = None, requester: Member = None) -> LoopType:
"""Sets the repeat mode for the queue."""
if not mode: if not mode:
mode = self.queue._repeat.next().name mode = self.queue._repeat.next()
is_found = False if not isinstance(mode, LoopType):
for type in LoopType:
if type.name.lower() == mode.lower():
self.queue._repeat.set_mode(type)
is_found = True
break
if not is_found:
raise VoicelinkException("Invalid repeat mode.") raise VoicelinkException("Invalid repeat mode.")
self.queue._repeat.set_mode(mode)
if self.is_ipc_connected: if self.is_ipc_connected:
await self.send_ws({"op": "repeatTrack", "repeatMode": mode}, requester) await self.send_ws({"op": "repeatTrack", "repeatMode": mode.name.lower()}, requester)
self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) has been update the repeat mode to {mode}.") self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) has been update the repeat mode to {mode.name.lower()}.")
return mode return mode
async def add_filter(self, filter: Filter, requester: Member = None, fast_apply: bool = False) -> Filters: async def add_filter(self, filter: Filter, requester: Member = None, fast_apply: bool = False) -> Filters:
"""Adds a filter to the player's audio stream."""
try: try:
self._filters.add_filter(filter=filter) self._filters.add_filter(filter=filter)
except FilterTagAlreadyInUse: except FilterTagAlreadyInUse:
raise FilterTagAlreadyInUse(self.get_msg("FilterTagAlreadyInUse")) raise FilterTagAlreadyInUse(self.get_msg("FilterTagAlreadyInUse"))
payload = self._filters.get_all_payloads() payload = self._filters.get_all_payloads()
await self.send(method=RequestMethod.patch, data={"filters": payload}) await self.send(method=RequestMethod.PATCH, data={"filters": payload})
if fast_apply: if fast_apply:
await self.seek(self.position) await self.seek(self.position)
@@ -710,6 +757,7 @@ class Player(VoiceProtocol):
return self._filters return self._filters
async def clear_queue(self, queue_type: str, requester: Member = None) -> None: async def clear_queue(self, queue_type: str, requester: Member = None) -> None:
"""Clears the queue or the history of tracks."""
queue_type = queue_type.lower() queue_type = queue_type.lower()
if queue_type == 'history': if queue_type == 'history':
self.queue.history_clear(self.is_playing) self.queue.history_clear(self.is_playing)
@@ -725,7 +773,7 @@ class Player(VoiceProtocol):
async def remove_filter(self, filter_tag: str, requester: Member = None, fast_apply: bool = False) -> Filters: async def remove_filter(self, filter_tag: str, requester: Member = None, fast_apply: bool = False) -> Filters:
self._filters.remove_filter(filter_tag=filter_tag) self._filters.remove_filter(filter_tag=filter_tag)
payload = self._filters.get_all_payloads() payload = self._filters.get_all_payloads()
await self.send(method=RequestMethod.patch, data={"filters": payload}) await self.send(method=RequestMethod.PATCH, data={"filters": payload})
if fast_apply: if fast_apply:
await self.seek(self.position) await self.seek(self.position)
@@ -740,11 +788,12 @@ class Player(VoiceProtocol):
return self._filters return self._filters
async def reset_filter(self, *, requester: Member = None, fast_apply=False) -> None: async def reset_filter(self, *, requester: Member = None, fast_apply=False) -> None:
"""Resets all filters applied to the player's audio stream."""
if not self._filters: if not self._filters:
raise FilterInvalidArgument("You must have filters applied first in order to use this method.") raise FilterInvalidArgument("You must have filters applied first in order to use this method.")
self._filters.reset_filters() self._filters.reset_filters()
await self.send(method=RequestMethod.patch, data={"filters": {}}) await self.send(method=RequestMethod.PATCH, data={"filters": {}})
if fast_apply: if fast_apply:
await self.seek(self.position) await self.seek(self.position)
@@ -757,7 +806,7 @@ class Player(VoiceProtocol):
self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) has been removed all filters.") self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) has been removed all filters.")
async def change_node(self, identifier: str = None) -> None: async def change_node(self, identifier: str = None) -> None:
"""Change node.""" """Changes the audio processing node for the guild.."""
try: try:
node = NodePool.get_node(identifier=identifier) node = NodePool.get_node(identifier=identifier)
except: except:
@@ -768,16 +817,13 @@ class Player(VoiceProtocol):
self._node._players[self.guild.id] = self self._node._players[self.guild.id] = self
await self._dispatch_voice_update(self._voice_state) await self._dispatch_voice_update(self._voice_state)
if self.current: if self.current:
await self.play(self.current, start=self.position) await self.play(self.current, start=self.position)
self._last_update = time.time() * 1000 self._last_update = time.time() * 1000
if self.is_paused: if self.is_paused:
await self.set_pause(True) await self.set_pause(True)
if self.volume != 100:
await self.set_volume(self.volume)
async def get_recommendations(self, *, track: Optional[Track] = None) -> bool: async def get_recommendations(self, *, track: Optional[Track] = None) -> bool:
"""Get recommendations from Youtube or Spotify.""" """Get recommendations from Youtube or Spotify."""
@@ -796,6 +842,7 @@ class Player(VoiceProtocol):
return False return False
async def update_voice_status(self, remove_status: bool = False) -> None: async def update_voice_status(self, remove_status: bool = False) -> None:
"""Updates the voice status of the channel based on the specified template."""
template = self.settings.get("stage_announce_template", func.settings.voice_status_template) template = self.settings.get("stage_announce_template", func.settings.voice_status_template)
if not template or not self.channel: if not template or not self.channel:
return return
@@ -815,6 +862,7 @@ class Player(VoiceProtocol):
) )
async def send_ws(self, payload, requester: Member = None): async def send_ws(self, payload, requester: Member = None):
"""Sends a WebSocket payload to the bot's IPC (Inter-Process Communication) system."""
payload['guild_id'] = str(self.guild.id) payload['guild_id'] = str(self.guild.id)
if requester: if requester:
payload['requester_id'] = str(requester.id) payload['requester_id'] = str(requester.id)

View File

@@ -144,9 +144,6 @@ class Node:
@property @property
def spotify_client(self) -> Optional[spotify.Client]: def spotify_client(self) -> Optional[spotify.Client]:
if not self._spotify_client: if not self._spotify_client:
if not self._spotify_client_id or not self._spotify_client_secret:
return None
self._spotify_client = spotify.Client( self._spotify_client = spotify.Client(
self._spotify_client_id, self._spotify_client_secret self._spotify_client_id, self._spotify_client_secret
) )
@@ -271,7 +268,7 @@ class Node:
if resp.status >= 300: if resp.status >= 300:
raise NodeException(f"Getting errors from Lavalink REST api") raise NodeException(f"Getting errors from Lavalink REST api")
if method == RequestMethod.delete: if method == RequestMethod.DELETE:
return await resp.json(content_type=None) return await resp.json(content_type=None)
return await resp.json() return await resp.json()
@@ -286,7 +283,7 @@ class Node:
self._task = self._bot.loop.create_task(self._listen()) self._task = self._bot.loop.create_task(self._listen())
self._available = True self._available = True
self._info = NodeInfo(await self.send(RequestMethod.get, query="info")) self._info = NodeInfo(await self.send(RequestMethod.GET, query="info"))
self._logger.info(f"Node [{self._identifier}] is connected!") self._logger.info(f"Node [{self._identifier}] is connected!")
@@ -369,7 +366,7 @@ class Node:
query: str, query: str,
*, *,
requester: Member, requester: Member,
search_type: SearchType = SearchType.ytsearch search_type: SearchType = SearchType.YOUTUBE
) -> Union[List[Track], Playlist]: ) -> Union[List[Track], Playlist]:
"""Fetches tracks from the node's REST api to parse into Lavalink. """Fetches tracks from the node's REST api to parse into Lavalink.
@@ -380,18 +377,15 @@ class Node:
Context object on any track you search. Context object on any track you search.
""" """
if not URL_REGEX.match(query) and not re.match(r"(?:ytm?|sc)search:.", query): if not URL_REGEX.match(query):
query = f"{search_type}:{query}" if search_type == SearchType.SPOTIFY:
return await self.spotifySearch(query=query, requester=requester)
else:
query = f"{search_type}:{query}"
if SPOTIFY_URL_REGEX.match(query): if SPOTIFY_URL_REGEX.match(query):
try: try:
if not self.spotify_client:
raise InvalidSpotifyClientAuthorization(
"You did not provide proper Spotify client authorization credentials. "
"If you would like to use the Spotify searching feature, "
"please obtain Spotify API credentials here: https://developer.spotify.com/"
)
spotify_results = await self.spotify_client.search(query=query) spotify_results = await self.spotify_client.search(query=query)
except Exception as _: except Exception as _:
raise TrackLoadError("Not able to find the provided Spotify entity, is it private?") raise TrackLoadError("Not able to find the provided Spotify entity, is it private?")
@@ -509,7 +503,7 @@ class Node:
Track( Track(
track_id=None, track_id=None,
requester=requester, requester=requester,
search_type=SearchType.ytsearch, search_type=SearchType.YOUTUBE,
spotify_track=track, spotify_track=track,
info=track.to_dict() info=track.to_dict()
) )
@@ -525,7 +519,7 @@ class Node:
tracks = [ tracks = [
Track( Track(
track_id=None, track_id=None,
search_type=SearchType.ytsearch, search_type=SearchType.YOUTUBE,
spotify_track=track, spotify_track=track,
info=track.to_dict(), info=track.to_dict(),
requester=self.bot.user requester=self.bot.user
@@ -572,12 +566,12 @@ class NodePool:
This option is preferred if you want to choose the best node This option is preferred if you want to choose the best node
from a multi-node setup using either the node's latency from a multi-node setup using either the node's latency
or the node's voice region. or the node's voice region.
Use NodeAlgorithm.by_ping if you want to get the best node Use NodeAlgorithm.BY_PING if you want to get the best node
based on the node's latency. based on the node's latency.
Use NodeAlgorithm.by_region if you want to get the best node Use NodeAlgorithm.by_region if you want to get the best node
based on the node's voice region. This method will only work based on the node's voice region. This method will only work
if you set a voice region when you create a node. if you set a voice region when you create a node.
Use NodeAlgorithm.by_players if you want to get the best node Use NodeAlgorithm.BY_PLAYERS if you want to get the best node
based on how players it has. This method will return a node with based on how players it has. This method will return a node with
the least amount of players the least amount of players
""" """
@@ -586,11 +580,11 @@ class NodePool:
if not available_nodes: if not available_nodes:
raise NoNodesAvailable("There are no nodes available.") raise NoNodesAvailable("There are no nodes available.")
if algorithm == NodeAlgorithm.by_ping: if algorithm == NodeAlgorithm.BY_PING:
tested_nodes = {node: node.latency for node in available_nodes} tested_nodes = {node: node.latency for node in available_nodes}
return min(tested_nodes, key=tested_nodes.get) return min(tested_nodes, key=tested_nodes.get)
elif algorithm == NodeAlgorithm.by_players: elif algorithm == NodeAlgorithm.BY_PLAYERS:
tested_nodes = {node: len(node.players.keys()) for node in available_nodes} tested_nodes = {node: len(node.players.keys()) for node in available_nodes}
return min(tested_nodes, key=tested_nodes.get) return min(tested_nodes, key=tested_nodes.get)

View File

@@ -25,7 +25,7 @@ from .exceptions import QueueFull, OutofList
from .objects import Track from .objects import Track
from .enums import LoopType from .enums import LoopType
from typing import Optional, Tuple, List, Callable, Dict from typing import Optional, Tuple, Callable, Dict, List
from itertools import cycle from itertools import cycle
from discord import Member from discord import Member
@@ -65,16 +65,16 @@ class Queue:
def get(self) -> Optional[Track]: def get(self) -> Optional[Track]:
track = None track = None
try: try:
track = self._queue[self._position - 1 if self._repeat.mode == LoopType.track else self._position] track = self._queue[self._position - 1 if self._repeat.mode == LoopType.TRACK else self._position]
if self._repeat.mode != LoopType.track: if self._repeat.mode != LoopType.TRACK:
self._position += 1 self._position += 1
except: except:
if self._repeat.mode == LoopType.queue: if self._repeat.mode == LoopType.QUEUE:
try: try:
track = self._queue[self._repeat_position] track = self._queue[self._repeat_position]
self._position = self._repeat_position + 1 self._position = self._repeat_position + 1
except IndexError: except IndexError:
self._repeat.set_mode(LoopType.off) self._repeat.set_mode(LoopType.OFF)
return track return track

View File

@@ -26,12 +26,19 @@ import time
import aiohttp import aiohttp
from base64 import b64encode from base64 import b64encode
from typing import List, Union, Dict, Any from typing import (
List,
Dict,
Union,
Optional
)
from .objects import Track, Album, Artist, Playlist, Category from .objects import Track, Album, Artist, Playlist, Category
from .exceptions import InvalidSpotifyURL, SpotifyRequestException from .exceptions import InvalidSpotifyURL, SpotifyRequestException
BASE_URL = "https://api.spotify.com/v1/" BASE_URL = "https://api.spotify.com/v1/"
GRANT_URL = "https://accounts.spotify.com/api/token" GRANT_URL = "https://accounts.spotify.com/api/token"
ANONYMOUS_GRANT_URL = "https://open.spotify.com/get_access_token"
REQUEST_URL = BASE_URL + "{type}s/{id}" REQUEST_URL = BASE_URL + "{type}s/{id}"
SEARCH_URL = BASE_URL + "search?q={query}&type={type}&limit={limit}" SEARCH_URL = BASE_URL + "search?q={query}&type={type}&limit={limit}"
SUGGESTION_URL = BASE_URL + "recommendations?limit={limit}&seed_tracks={seed_tracks}" SUGGESTION_URL = BASE_URL + "recommendations?limit={limit}&seed_tracks={seed_tracks}"
@@ -46,35 +53,45 @@ class Client:
""" """
def __init__(self, client_id: str, client_secret: str) -> None: def __init__(self, client_id: str, client_secret: str) -> None:
self._client_id: str = client_id self._client_id: Optional[str] = client_id
self._client_secret: str = client_secret self._client_secret: Optional[str] = client_secret
self.session: aiohttp.ClientSession = aiohttp.ClientSession() self.session: aiohttp.ClientSession = aiohttp.ClientSession()
self._bearer_token: str = None self._bearer_token: str = None
self._expiry: int = 0 self._expiry: int = 0
self._auth_token: str = b64encode(f"{self._client_id}:{self._client_secret}".encode()) self._auth_token: bytes = b64encode(f"{self._client_id}:{self._client_secret}".encode())
self._grant_headers: Dict[str, str] = {"Authorization": f"Basic {self._auth_token.decode()}"} self._grant_headers: Dict[str, str] = {"Authorization": f"Basic {self._auth_token.decode()}"}
self._bearer_headers: Dict[str, str] = None self._bearer_headers: Dict[str, str] = None
self._categories: List[Category] = [] self._categories: List[Category] = []
async def _fetch_bearer_token(self) -> None: async def _fetch_bearer_token(self) -> None:
_data = {"grant_type": "client_credentials"} """Fetches and stores a bearer token for API authentication."""
if self._client_id and self._client_secret:
url, data = GRANT_URL, {"grant_type": "client_credentials"}
else:
url, data = ANONYMOUS_GRANT_URL, None
async with self.session.post(GRANT_URL, data=_data, headers=self._grant_headers) as resp: async with self.session.post(url, data=data, headers=self._grant_headers) if data else self.session.get(url) as resp:
if resp.status != 200: if resp.status != 200:
raise SpotifyRequestException( raise SpotifyRequestException(
f"Error fetching bearer token: {resp.status} {resp.reason}" f"Error fetching bearer token: {resp.status} {resp.reason}"
) )
data: Dict = await resp.json() response_data: Dict = await resp.json()
if self._client_id and self._client_secret:
self._bearer_token = response_data["access_token"]
self._expiry = time.time() + int(response_data["expires_in"]) - 10
else:
self._bearer_token = response_data["accessToken"]
self._expiry = response_data["accessTokenExpirationTimestampMs"] / 1000
self._bearer_token = data["access_token"]
self._expiry = time.time() + (int(data["expires_in"]) - 10)
self._bearer_headers = {"Authorization": f"Bearer {self._bearer_token}"} self._bearer_headers = {"Authorization": f"Bearer {self._bearer_token}"}
async def get_request(self, url: str) -> Dict: async def get_request(self, url: str) -> Dict:
"""Performs a GET request to the specified URL with authorization headers."""
if not self._bearer_token or time.time() >= self._expiry: if not self._bearer_token or time.time() >= self._expiry:
await self._fetch_bearer_token() await self._fetch_bearer_token()
@@ -87,24 +104,27 @@ class Client:
return await resp.json() return await resp.json()
async def track_search(self, query: str, track: str = "track", limit: int = 10) -> List[Track]: async def track_search(self, query: str, track: str = "track", limit: int = 10) -> List[Track]:
"""Searches for tracks based on the provided query and returns a list of Track objects."""
request_url = SEARCH_URL.format(query=query, type=track, limit=limit) request_url = SEARCH_URL.format(query=query, type=track, limit=limit)
data = await self.get_request(request_url) data = await self.get_request(request_url)
return [ Track(track) for track in data['tracks']['items'] ] return [ Track(track) for track in data['tracks']['items'] ]
async def similar_track(self, seed_tracks: str, *, limit: int = 10) -> List[Track]: async def similar_track(self, seed_tracks: str, *, limit: int = 10) -> List[Track]:
"""Retrieves tracks similar to the provided seed tracks and returns them as Track objects."""
request_url = SUGGESTION_URL.format(limit=limit, seed_tracks=seed_tracks) request_url = SUGGESTION_URL.format(limit=limit, seed_tracks=seed_tracks)
data = await self.get_request(request_url) data = await self.get_request(request_url)
return [ Track(track) for track in data['tracks'] ] return [ Track(track) for track in data['tracks'] ]
async def search(self, *, query: str) -> Union[Track, Album, Playlist]: async def search(self, *, query: str) -> Union[Track, Album, Playlist]:
"""Searches for an item (track, album, artist, or playlist) by query and returns the corresponding object."""
result = SPOTIFY_URL_REGEX.match(query) result = SPOTIFY_URL_REGEX.match(query)
spotify_type = result.group("type")
spotify_id = result.group("id")
if not result: if not result:
raise InvalidSpotifyURL("The Spotify link provided is not valid.") raise InvalidSpotifyURL("The Spotify link provided is not valid.")
spotify_type = result.group("type")
spotify_id = result.group("id")
request_url = REQUEST_URL.format(type=spotify_type, id=spotify_id) request_url = REQUEST_URL.format(type=spotify_type, id=spotify_id)
if isArtist := (spotify_type == "artist"): if isArtist := (spotify_type == "artist"):
request_url += "/top-tracks?market=US" request_url += "/top-tracks?market=US"
@@ -116,41 +136,40 @@ class Client:
return Album(data) return Album(data)
elif isArtist: elif isArtist:
return Artist(data) return Artist(data)
else:
tracks = [ tracks = [
Track(track["track"])
for track in data["tracks"]["items"] if track.get("track") is not None
]
if not tracks:
raise SpotifyRequestException("This playlist is empty and therefore cannot be queued.")
next_page_url = data["tracks"].get("next")
while next_page_url:
next_data = await self.get_request(next_page_url)
tracks.extend([
Track(track["track"]) Track(track["track"])
for track in data["tracks"]["items"] if track["track"] is not None for track in next_data.get("items", []) if track.get("track") is not None
] ])
next_page_url = next_data.get("next")
if not tracks: return Playlist(data, tracks)
raise SpotifyRequestException("This playlist is empty and therefore cannot be queued.")
next_page_url = data["tracks"]["next"]
while next_page_url is not None:
async with self.session.get(next_page_url, headers=self._bearer_headers) as resp:
if resp.status != 200:
raise SpotifyRequestException(
f"Error while fetching results: {resp.status} {resp.reason}"
)
next_data: Dict = await resp.json()
tracks += [
Track(track["track"])
for track in next_data["items"] if track["track"] is not None
]
next_page_url = next_data["next"]
return Playlist(data, tracks)
async def get_categories(self) -> List[Category]: async def get_categories(self) -> List[Category]:
"""Fetches and returns available music categories from the Spotify API."""
if not self._categories: if not self._categories:
request_url = f"{BASE_URL}browse/categories" request_url = f"{BASE_URL}browse/categories"
data = await self.get_request(request_url)
self._categories = [Category(item) for item in data.get("items", [])] while request_url:
data = await self.get_request(request_url)
items = data.get("categories", {}).get("items", [])
self._categories.extend(Category(item) for item in items)
request_url = data.get("categories", {}).get("next")
return self._categories return self._categories
async def close(self) -> None: async def close(self) -> None:
"""Closes the HTTP session used for making API requests."""
await self.session.close() await self.session.close()

View File

@@ -129,7 +129,7 @@ class Category:
self.href: str = data.get("href") self.href: str = data.get("href")
self.id: str = data.get("id") self.id: str = data.get("id")
self.name: str = data.get("name") self.name: str = data.get("name")
self.icon: str = data.get("icon", [])[0].get("url") self.icon: str = data.get("icons", [{}])[0].get("url")
def __repr__(self) -> str: def __repr__(self) -> str:
return (f"<Voicelink.spotify.Category name={self.name} id={self.id}") return (f"<Voicelink.spotify.Category name={self.name} id={self.id}")