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