Compare commits

..

No commits in common. "316d0ff3f4a0efe7dbc60b94caf1155f6d0fb6d1" and "393403ef7d8946df21121004eeba4a6a7467dee6" have entirely different histories.

8 changed files with 98 additions and 182 deletions

View File

@ -17,11 +17,8 @@ class ArgumentParser:
self.parser.print_help() self.parser.print_help()
return help_buffer.getvalue().replace(" and exit", "") return help_buffer.getvalue().replace(" and exit", "")
def add_mutually_exclusive_group(self, *args, **kwargs):
return self.parser.add_mutually_exclusive_group(*args, **kwargs)
def add_argument(self, *args, **kwargs): def add_argument(self, *args, **kwargs):
return self.parser.add_argument(*args, **kwargs) self.parser.add_argument(*args, **kwargs)
async def parse_args(self, message, tokens) -> argparse.Namespace | None: async def parse_args(self, message, tokens) -> argparse.Namespace | None:
try: try:

View File

@ -1,7 +1,6 @@
import re import re
import arguments import arguments
import commands import commands
import utils import utils

View File

@ -39,30 +39,30 @@ def match(command: str) -> None | list[Command]:
def tokenize(string: str) -> list[str]: def tokenize(string: str) -> list[str]:
tokens = [] tokens = []
token = "" current_token = []
in_quotes = False in_quotes = False
quote_char = None escape_next = False
escape = False
for char in string[len(constants.PREFIX) :]: for char in string[len(constants.PREFIX) :]:
if escape: if escape_next:
token += char current_token.append(char)
escape = False escape_next = False
elif char == "\\": elif char == "\\":
escape = True escape_next = True
elif char in ('"', "'") and not in_quotes: elif char in ['"', "'"]:
in_quotes = True if in_quotes:
quote_char = char if current_token and current_token[0] == char:
elif char == quote_char and in_quotes:
in_quotes = False in_quotes = False
quote_char = None
elif char.isspace() and not in_quotes:
if token:
tokens.append(token)
token = ""
else: else:
token += char in_quotes = True
elif char.isspace() and not in_quotes:
if current_token:
tokens.append("".join(current_token))
current_token = []
else:
current_token.append(char)
if current_token:
tokens.append("".join(current_token))
if token:
tokens.append(token)
return tokens return tokens

View File

@ -4,7 +4,7 @@ import arguments
import commands import commands
import utils import utils
import youtubedl import youtubedl
from state import client, players from state import client, player_current, player_queue
async def queue_or_play(message): async def queue_or_play(message):
@ -12,16 +12,21 @@ async def queue_or_play(message):
if not command_allowed(message): if not command_allowed(message):
return return
if message.guild.id not in players: if message.guild.id not in player_queue:
players[message.guild.id] = youtubedl.QueuedPlayer() player_queue[message.guild.id] = []
tokens = commands.tokenize(message.content) tokens = commands.tokenize(message.content)
parser = arguments.ArgumentParser( parser = arguments.ArgumentParser(
tokens[0], "queue a song, list the queue, or resume playback" tokens[0], "queue a song, list the queue, or resume playback"
) )
parser.add_argument("query", nargs="?", help="yt-dlp URL or query to get song") parser.add_argument("query", nargs="?", help="yt-dlp URL or query to get song")
group = parser.add_mutually_exclusive_group() parser.add_argument(
group.add_argument( "-c",
"--clear",
action="store_true",
help="clear all queued songs",
)
parser.add_argument(
"-v", "-v",
"--volume", "--volume",
default=50, default=50,
@ -29,68 +34,13 @@ async def queue_or_play(message):
metavar="[0-150]", metavar="[0-150]",
help="the volume level (0 - 150)", help="the volume level (0 - 150)",
) )
group.add_argument(
"-i",
"--remove-index",
type=int,
help="remove a queued song by index",
)
group.add_argument(
"-m",
"--remove-multiple",
action="store_true",
help="continue removing queued songs after finding a match",
)
group.add_argument(
"-c",
"--clear",
action="store_true",
help="remove all queued songs",
)
parser.add_argument(
"-t",
"--remove-title",
help="remove queued songs by title",
)
parser.add_argument(
"-q",
"--remove-queuer",
type=int,
help="remove queued songs by queuer",
)
if not (args := await parser.parse_args(message, tokens)): if not (args := await parser.parse_args(message, tokens)):
return return
if args.clear: if args.clear:
players[message.guild.id].queue.clear() player_queue[message.guild.id] = []
await utils.add_check_reaction(message) await utils.add_check_reaction(message)
return return
elif i := args.remove_index:
try:
queued = players[message.guild.id].queue[i - 1]
del players[message.guild.id].queue[i - 1]
await utils.reply(message, f"**x** {queued.format()}")
except:
await utils.reply(message, "invalid index!")
elif args.remove_title or args.remove_queuer:
targets = []
for queued in players[message.guild.id].queue:
if t := args.remove_title:
if t in queued.player.title:
targets.append(queued)
continue
if q := args.remove_queuer:
if q == queued.queuer:
targets.append(queued)
if args.remove_multiple:
targets = targets[:1]
for target in targets:
players[message.guild.id].queue.remove(target)
await utils.reply(
message,
f"removed **{len(targets)}** queued {'song' if len(targets) == 1 else 'songs'}",
)
elif query := args.query: elif query := args.query:
try: try:
async with message.channel.typing(): async with message.channel.typing():
@ -105,21 +55,23 @@ async def queue_or_play(message):
) )
return return
queued = youtubedl.QueuedSong(player, message.author.id) player_queue[message.guild.id].insert(
players[message.guild.id].queue_add(queued) 0, {"player": player, "queuer": message.author.id}
)
if ( if (
not message.guild.voice_client.is_playing() not message.guild.voice_client.is_playing()
and not message.guild.voice_client.is_paused() and not message.guild.voice_client.is_paused()
): ):
await utils.reply(message, f"**0.** {queued.format()}") await utils.reply(message, f"**now playing:** `{player.title}`")
play_next(message) play_next(message)
else: else:
await utils.reply( await utils.reply(
message, message,
f"**+** {queued.format()}", f"**+** `{player.title}`",
) )
else: else:
if message.guild.voice_client:
if tokens[0].lower() == "play": if tokens[0].lower() == "play":
message.guild.voice_client.resume() message.guild.voice_client.resume()
await utils.reply( await utils.reply(
@ -127,34 +79,35 @@ async def queue_or_play(message):
"resumed!", "resumed!",
) )
else: else:
currently_playing = ( generate_currently_playing = (
lambda: f"**0.** {'(paused) ' if message.guild.voice_client.is_paused() else ''} {players[message.guild.id].current.format(with_queuer=True)}" lambda: f"**0.** {'**paused:** ' if message.guild.voice_client.is_paused() else ''}`{message.guild.voice_client.source.title}` (<@{player_current[message.guild.id]['queuer']}>)"
)
queue_list = lambda: "\n".join(
[
f"**{i + 1}.** {queued.format(with_queuer=True, hide_preview=True)}"
for i, queued in enumerate(players[message.guild.id].queue)
]
) )
if ( if (
not players[message.guild.id].queue not player_queue[message.guild.id]
and not message.guild.voice_client.source and not message.guild.voice_client.source
): ):
await utils.reply( await utils.reply(
message, message,
"nothing is playing or queued!", "nothing is playing or queued!",
) )
elif not players[message.guild.id].queue: elif not player_queue[message.guild.id]:
await utils.reply(message, currently_playing()) await utils.reply(message, generate_currently_playing())
elif not message.guild.voice_client.source: elif not message.guild.voice_client.source:
await utils.reply( await utils.reply(
message, message,
queue_list(), generate_queue_list(player_queue[message.guild.id]),
) )
else: else:
await utils.reply( await utils.reply(
message, message,
currently_playing() + "\n" + queue_list(), generate_currently_playing()
+ "\n"
+ generate_queue_list(player_queue[message.guild.id]),
)
else:
await utils.reply(
message,
"nothing is currently queued!",
) )
@ -162,7 +115,7 @@ async def skip(message):
if not command_allowed(message): if not command_allowed(message):
return return
if not players[message.guild.id].queue: if not player_queue[message.guild.id]:
message.guild.voice_client.stop() message.guild.voice_client.stop()
await utils.reply( await utils.reply(
message, message,
@ -191,13 +144,10 @@ async def resume(message):
if not command_allowed(message): if not command_allowed(message):
return return
if message.guild.voice_client.is_paused():
message.guild.voice_client.resume() message.guild.voice_client.resume()
await utils.add_check_reaction(message)
else:
await utils.reply( await utils.reply(
message, message,
"nothing is paused!", "resumed!",
) )
@ -205,13 +155,10 @@ async def pause(message):
if not command_allowed(message): if not command_allowed(message):
return return
if message.guild.voice_client.is_playing():
message.guild.voice_client.pause() message.guild.voice_client.pause()
await utils.add_check_reaction(message)
else:
await utils.reply( await utils.reply(
message, message,
"nothing is playing!", "paused!",
) )
@ -237,7 +184,7 @@ async def volume(message):
if not message.guild.voice_client.source: if not message.guild.voice_client.source:
await utils.reply( await utils.reply(
message, message,
f"nothing is playing!", f"there is no player currently active!",
) )
return return
@ -251,19 +198,13 @@ async def volume(message):
await utils.add_check_reaction(message) await utils.add_check_reaction(message)
def play_after_callback(e, message, once):
if e:
print(f"player error: {e}")
if not once:
play_next(message)
def play_next(message, once=False): def play_next(message, once=False):
message.guild.voice_client.stop() message.guild.voice_client.stop()
if players[message.guild.id].queue: if player_queue[message.guild.id]:
queued = players[message.guild.id].queue_pop() queued = player_queue[message.guild.id].pop()
player_current[message.guild.id] = queued
message.guild.voice_client.play( message.guild.voice_client.play(
queued.player, after=lambda e: play_after_callback(e, message, once) queued["player"], after=lambda _: play_next(message) if not once else None
) )
@ -279,3 +220,12 @@ def command_allowed(message):
if not message.guild.voice_client: if not message.guild.voice_client:
return False return False
return message.author.voice.channel.id == message.guild.voice_client.channel.id return message.author.voice.channel.id == message.guild.voice_client.channel.id
def generate_queue_list(queue: list):
return "\n".join(
[
f"**{i + 1}.** `{queued['player'].title}` (<@{queued['queuer']}>)"
for i, queued in enumerate(queue)
]
)

View File

@ -13,7 +13,6 @@ RELOADABLE_MODULES = [
"constants", "constants",
"core", "core",
"events", "events",
"player",
"utils", "utils",
"voice", "voice",
"youtubedl", "youtubedl",

View File

@ -30,9 +30,6 @@ async def on_message(message):
) )
return return
if message.guild.id not in command_locks:
command_locks[message.guild.id] = asyncio.Lock()
C = commands.Command C = commands.Command
try: try:
match matched[0]: match matched[0]:
@ -98,6 +95,8 @@ async def on_message(message):
case C.LEAVE: case C.LEAVE:
await commands.voice.leave(message) await commands.voice.leave(message)
case C.QUEUE | C.PLAY: case C.QUEUE | C.PLAY:
if message.guild.id not in command_locks:
command_locks[message.guild.id] = asyncio.Lock()
async with command_locks[message.guild.id]: async with command_locks[message.guild.id]:
await commands.voice.queue_or_play(message) await commands.voice.queue_or_play(message)
case C.SKIP: case C.SKIP:

View File

@ -2,7 +2,8 @@ import time
import disnake import disnake
players = {} player_queue = {}
player_current = {}
command_locks = {} command_locks = {}
intents = disnake.Intents.default() intents = disnake.Intents.default()

View File

@ -14,9 +14,7 @@ class YTDLSource(disnake.PCMVolumeTransformer):
self, source: disnake.AudioSource, *, data: dict[str, Any], volume: float = 0.5 self, source: disnake.AudioSource, *, data: dict[str, Any], volume: float = 0.5
): ):
super().__init__(source, volume) super().__init__(source, volume)
print(data)
self.title = data.get("title") self.title = data.get("title")
self.original_url = data.get("original_url")
@classmethod @classmethod
async def from_url( async def from_url(
@ -43,33 +41,6 @@ class YTDLSource(disnake.PCMVolumeTransformer):
) )
class QueuedPlayer:
def __init__(self):
self.queue = []
self.current = None
def queue_pop(self):
popped = self.queue[0]
del self.queue[0]
self.current = popped
return popped
def queue_add(self, item):
self.queue.append(item)
class QueuedSong:
def __init__(self, player, queuer):
self.player = player
self.queuer = queuer
def format(self, with_queuer=False, hide_preview=False):
return (
f"[`{self.player.title}`]({'<' if hide_preview else ''}{self.player.original_url}{'>' if hide_preview else ''})"
+ (f" (<@{self.queuer}>)" if with_queuer else "")
)
def __reload_module__(): def __reload_module__():
global ytdl global ytdl
ytdl = yt_dlp.YoutubeDL(constants.YTDL_OPTIONS) ytdl = yt_dlp.YoutubeDL(constants.YTDL_OPTIONS)