1
Fork 0
mirror of https://github.com/RYGhub/royalnet.git synced 2024-12-17 23:24:20 +00:00

It plays music now!

This commit is contained in:
Steffo 2019-04-17 01:15:35 +02:00
parent 3e27e3b183
commit aa48c3d1e2
7 changed files with 85 additions and 26 deletions

View file

@ -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):

View file

@ -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)

View file

@ -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)

View file

@ -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):

View file

@ -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].")

View file

@ -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)

View file

@ -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: