mirror of
https://github.com/RYGhub/royalnet.git
synced 2024-11-27 13:34:28 +00:00
It plays music now!
This commit is contained in:
parent
3e27e3b183
commit
aa48c3d1e2
7 changed files with 85 additions and 26 deletions
|
@ -3,12 +3,19 @@ import random
|
|||
|
||||
|
||||
class PlayMode:
|
||||
def __init__(self):
|
||||
self.now_playing = None
|
||||
self.generator = self._generator()
|
||||
|
||||
async def next(self):
|
||||
return await self.generator.__anext__()
|
||||
|
||||
def videos_left(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def next(self):
|
||||
async def _generator(self):
|
||||
"""Get the next video from the list and advance it."""
|
||||
raise NotImplementedError()
|
||||
yield NotImplemented
|
||||
|
||||
def add(self, item):
|
||||
"""Add a new video to the PlayMode."""
|
||||
|
@ -18,6 +25,7 @@ class PlayMode:
|
|||
class Playlist(PlayMode):
|
||||
"""A video list. Videos played are removed from the list."""
|
||||
def __init__(self, starting_list=None):
|
||||
super().__init__()
|
||||
if starting_list is None:
|
||||
starting_list = []
|
||||
self.list = starting_list
|
||||
|
@ -25,14 +33,15 @@ class Playlist(PlayMode):
|
|||
def videos_left(self):
|
||||
return len(self.list)
|
||||
|
||||
async def next(self):
|
||||
async def _generator(self):
|
||||
while True:
|
||||
try:
|
||||
next_video = self.list.pop(0)
|
||||
except IndexError:
|
||||
yield None
|
||||
self.now_playing = None
|
||||
else:
|
||||
yield next_video
|
||||
self.now_playing = next_video
|
||||
yield self.now_playing
|
||||
|
||||
def add(self, item):
|
||||
self.list.append(item)
|
||||
|
@ -41,6 +50,7 @@ class Playlist(PlayMode):
|
|||
class Pool(PlayMode):
|
||||
"""A video pool. Videos played are played back in random order, and they are kept in the pool."""
|
||||
def __init__(self, starting_pool=None):
|
||||
super().__init__()
|
||||
if starting_pool is None:
|
||||
starting_pool = []
|
||||
self.pool = starting_pool
|
||||
|
@ -49,15 +59,17 @@ class Pool(PlayMode):
|
|||
def videos_left(self):
|
||||
return math.inf
|
||||
|
||||
async def next(self):
|
||||
async def _generator(self):
|
||||
while True:
|
||||
if self.pool:
|
||||
self.now_playing = None
|
||||
yield None
|
||||
continue
|
||||
self._pool_copy = self.pool.copy()
|
||||
random.shuffle(self._pool_copy)
|
||||
while self._pool_copy:
|
||||
next_video = self._pool_copy.pop(0)
|
||||
self.now_playing = next_video
|
||||
yield next_video
|
||||
|
||||
def add(self, item):
|
||||
|
|
|
@ -19,10 +19,10 @@ class RoyalAudioFile(YtdlFile):
|
|||
def __init__(self, info: "YtdlInfo", **ytdl_args):
|
||||
# Overwrite the new ytdl_args
|
||||
self.ytdl_args = {**self.ytdl_args, **ytdl_args}
|
||||
super().__init__(info, outtmpl="%(title)s-%(id)s.%(ext)s", **self.ytdl_args)
|
||||
super().__init__(info, outtmpl="./opusfiles/%(title)s-%(id)s.%(ext)s", **self.ytdl_args)
|
||||
# Find the audio_filename with a regex (should be video.opus)
|
||||
self.audio_filename = re.sub(rf"\.{self.info.ext}$", ".mp3", self.video_filename)
|
||||
# Convert the video to mp3
|
||||
self.audio_filename = re.sub(rf"\.{self.info.ext}$", ".opus", self.video_filename)
|
||||
# Convert the video to opus
|
||||
# Actually not needed, but we do this anyways for compression reasons
|
||||
converter = ffmpeg.input(self.video_filename) \
|
||||
.output(self.audio_filename)
|
||||
|
|
|
@ -6,9 +6,9 @@ import sys
|
|||
from ..commands import NullCommand
|
||||
from ..utils import asyncify, Call, Command
|
||||
from ..error import UnregisteredError, NoneFoundError, TooManyFoundError
|
||||
from ..network import RoyalnetLink, Message, RequestSuccessful
|
||||
from ..network import RoyalnetLink, Message, RequestSuccessful, RequestError
|
||||
from ..database import Alchemy, relationshiplinkchain
|
||||
from ..audio import RoyalAudioFile
|
||||
from ..audio import RoyalAudioFile, PlayMode, Playlist, Pool
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
log = _logging.getLogger(__name__)
|
||||
|
@ -19,9 +19,9 @@ if not discord.opus.is_loaded():
|
|||
|
||||
|
||||
class PlayMessage(Message):
|
||||
def __init__(self, url: str, channel_identifier: typing.Optional[typing.Union[int, str]] = None):
|
||||
def __init__(self, url: str, guild_identifier: typing.Optional[str] = None):
|
||||
self.url: str = url
|
||||
self.channel_identifier: typing.Optional[typing.Union[int, str]] = channel_identifier
|
||||
self.guild_identifier: typing.Optional[str] = guild_identifier
|
||||
|
||||
|
||||
class SummonMessage(Message):
|
||||
|
@ -62,6 +62,8 @@ class DiscordBot:
|
|||
self.network: RoyalnetLink = RoyalnetLink(master_server_uri, master_server_secret, "discord",
|
||||
self.network_handler)
|
||||
loop.create_task(self.network.run())
|
||||
# Create the PlayModes dictionary
|
||||
self.music_data: typing.Dict[discord.Guild, PlayMode] = {}
|
||||
|
||||
# noinspection PyMethodParameters
|
||||
class DiscordCall(Call):
|
||||
|
@ -86,6 +88,8 @@ class DiscordBot:
|
|||
|
||||
async def net_request(call, message: Message, destination: str):
|
||||
response = await self.network.request(message, destination)
|
||||
if isinstance(response, RequestError):
|
||||
raise response.exc
|
||||
return response
|
||||
|
||||
async def get_author(call, error_if_none=False):
|
||||
|
@ -106,6 +110,7 @@ class DiscordBot:
|
|||
class DiscordClient(discord.Client):
|
||||
@staticmethod
|
||||
async def vc_connect_or_move(channel: discord.VoiceChannel):
|
||||
# Connect to voice chat
|
||||
try:
|
||||
await channel.connect()
|
||||
except discord.errors.ClientException:
|
||||
|
@ -116,6 +121,9 @@ class DiscordBot:
|
|||
if voice_client.guild != channel.guild:
|
||||
continue
|
||||
await voice_client.move_to(channel)
|
||||
# Create a music_data entry, if it doesn't exist; default is a Playlist
|
||||
if not self.music_data.get(channel.guild):
|
||||
self.music_data[channel.guild] = Playlist()
|
||||
|
||||
async def on_message(cli, message: discord.Message):
|
||||
text = message.content
|
||||
|
@ -146,7 +154,7 @@ class DiscordBot:
|
|||
self.DiscordClient = DiscordClient
|
||||
self.bot = self.DiscordClient()
|
||||
|
||||
def find_guild(self, identifier: typing.Union[str, int]):
|
||||
def find_guild(self, identifier: typing.Union[str, int]) -> discord.Guild:
|
||||
"""Find the Guild with the specified identifier. Names are case-insensitive."""
|
||||
if isinstance(identifier, str):
|
||||
all_guilds: typing.List[discord.Guild] = self.bot.guilds
|
||||
|
@ -163,7 +171,9 @@ class DiscordBot:
|
|||
return self.bot.get_guild(identifier)
|
||||
raise TypeError("Invalid identifier type, should be str or int")
|
||||
|
||||
def find_channel(self, identifier: typing.Union[str, int], guild: typing.Optional[discord.Guild] = None):
|
||||
def find_channel(self,
|
||||
identifier: typing.Union[str, int],
|
||||
guild: typing.Optional[discord.Guild] = None) -> discord.abc.GuildChannel:
|
||||
"""Find the GuildChannel with the specified identifier. Names are case-insensitive."""
|
||||
if isinstance(identifier, str):
|
||||
if guild is not None:
|
||||
|
@ -193,6 +203,13 @@ class DiscordBot:
|
|||
return channel
|
||||
raise TypeError("Invalid identifier type, should be str or int")
|
||||
|
||||
def find_voice_client(self, guild: discord.Guild):
|
||||
for voice_client in self.bot.voice_clients:
|
||||
voice_client: discord.VoiceClient
|
||||
if voice_client.guild == guild:
|
||||
return voice_client
|
||||
raise NoneFoundError("No voice clients found")
|
||||
|
||||
async def network_handler(self, message: Message) -> Message:
|
||||
"""Handle a Royalnet request."""
|
||||
if isinstance(message, SummonMessage):
|
||||
|
@ -208,9 +225,39 @@ class DiscordBot:
|
|||
loop.create_task(self.bot.vc_connect_or_move(channel))
|
||||
return RequestSuccessful()
|
||||
|
||||
async def add_to_music_data(self, url: str, guild: discord.Guild):
|
||||
"""Add a file to the corresponding music_data object."""
|
||||
files: typing.List[RoyalAudioFile] = await asyncify(RoyalAudioFile.create_from_url, url)
|
||||
guild_music_data = self.music_data[guild]
|
||||
for file in files:
|
||||
guild_music_data.add(file)
|
||||
if guild_music_data.now_playing is None:
|
||||
await self.advance_music_data(guild)
|
||||
|
||||
async def advance_music_data(self, guild: discord.Guild):
|
||||
guild_music_data = self.music_data[guild]
|
||||
next_file = await guild_music_data.next()
|
||||
if next_file is None:
|
||||
return
|
||||
voice_client = self.find_voice_client(guild)
|
||||
voice_client.play(next_file.as_audio_source(), after=lambda: loop.create_task(self.advance_music_data(guild)))
|
||||
|
||||
async def nh_play(self, message: PlayMessage):
|
||||
"""Handle a play Royalnet request. That is, add audio to a PlayMode."""
|
||||
raise
|
||||
# Find the matching guild
|
||||
if message.guild_identifier:
|
||||
guild = self.find_guild(message.guild_identifier)
|
||||
else:
|
||||
if len(self.music_data) != 1:
|
||||
raise TooManyFoundError("Multiple guilds found")
|
||||
guild = list(self.music_data)[0]
|
||||
# Ensure the guild has a PlayMode before adding the file to it
|
||||
if not self.music_data.get(guild):
|
||||
# TODO: change Exception
|
||||
raise Exception("No music_data for this guild")
|
||||
# Start downloading
|
||||
loop.create_task(self.add_to_music_data(message.url, guild))
|
||||
return RequestSuccessful()
|
||||
|
||||
async def run(self):
|
||||
await self.bot.login(self.token)
|
||||
|
|
|
@ -5,8 +5,8 @@ import logging as _logging
|
|||
import sys
|
||||
from ..commands import NullCommand
|
||||
from ..utils import asyncify, Call, Command
|
||||
from royalnet.error import UnregisteredError
|
||||
from ..network import RoyalnetLink, Message
|
||||
from ..error import UnregisteredError
|
||||
from ..network import RoyalnetLink, Message, RequestError
|
||||
from ..database import Alchemy, relationshiplinkchain
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
@ -71,6 +71,8 @@ class TelegramBot:
|
|||
|
||||
async def net_request(call, message: Message, destination: str):
|
||||
response = await self.network.request(message, destination)
|
||||
if isinstance(response, RequestError):
|
||||
raise response.exc
|
||||
return response
|
||||
|
||||
async def get_author(call, error_if_none=False):
|
||||
|
|
|
@ -7,11 +7,11 @@ from ..bots.discord import PlayMessage
|
|||
class PlayCommand(Command):
|
||||
command_name = "play"
|
||||
command_description = "Riproduce una canzone in chat vocale."
|
||||
command_syntax = "(url)"
|
||||
command_syntax = "[ [guild] ] (url)"
|
||||
|
||||
@classmethod
|
||||
async def common(cls, call: Call):
|
||||
url: str = call.args[0]
|
||||
response: typing.Union[RequestSuccessful, RequestError] = await call.net_request(PlayMessage(url), "discord")
|
||||
guild, url = call.args.match(r"(?:\[(.+)])?\s*(\S+)\s*")
|
||||
response: typing.Union[RequestSuccessful, RequestError] = await call.net_request(PlayMessage(url, guild), "discord")
|
||||
response.raise_on_error()
|
||||
await call.reply(f"✅ Richiesta la riproduzione di [c]{url}[/c].")
|
||||
|
|
|
@ -12,9 +12,7 @@ class ReminderCommand(Command):
|
|||
|
||||
@classmethod
|
||||
async def common(cls, call: Call):
|
||||
match = call.args.match(r"\[ *(.+?) *] *(.+?) *$")
|
||||
date_str = match.group(1)
|
||||
reminder_text = match.group(2)
|
||||
date_str, reminder_text = call.args.match(r"\[ *(.+?) *] *(.+?) *$")
|
||||
date: typing.Optional[datetime.datetime]
|
||||
try:
|
||||
date = dateparser.parse(date_str)
|
||||
|
|
|
@ -28,12 +28,12 @@ class CommandArgs(list):
|
|||
raise InvalidInputError("Not enough arguments")
|
||||
return " ".join(self)
|
||||
|
||||
def match(self, pattern: typing.Pattern) -> typing.Match:
|
||||
def match(self, pattern: typing.Pattern) -> typing.Sequence[typing.AnyStr]:
|
||||
text = self.joined()
|
||||
match = re.match(pattern, text)
|
||||
if match is None:
|
||||
raise InvalidInputError("Pattern didn't match")
|
||||
return match
|
||||
return match.groups()
|
||||
|
||||
def optional(self, index: int, default=None) -> typing.Optional:
|
||||
try:
|
||||
|
|
Loading…
Reference in a new issue