1
Fork 0
mirror of https://github.com/RYGhub/royalnet.git synced 2024-11-23 19:44:20 +00:00

IT WOOOOOOOOOOOOOOORKS

This commit is contained in:
Steffo 2019-11-23 02:04:30 +01:00
parent 2b24d8cd09
commit 88a9ef35ca
14 changed files with 121 additions and 120 deletions

View file

@ -3,7 +3,6 @@
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/royalnet.iml" filepath="$PROJECT_DIR$/.idea/royalnet.iml" />
<module fileurl="file://$PROJECT_DIR$/../royalpack/.idea/royalpack.iml" filepath="$PROJECT_DIR$/../royalpack/.idea/royalpack.iml" />
</modules>
</component>
</project>

View file

@ -9,7 +9,6 @@
</content>
<orderEntry type="jdk" jdkName="Python 3.8 (royalnet-1MWM6-kd-py3.8)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module" module-name="royalpack" />
</component>
<component name="TestRunnerService">
<option name="PROJECT_TEST_RUNNER" value="Unittests" />

View file

@ -3,6 +3,5 @@
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/docs_source/royalpack" vcs="Git" />
<mapping directory="$PROJECT_DIR$/../royalpack" vcs="Git" />
</component>
</project>

View file

@ -1,30 +1,26 @@
from royalnet.commands import *
from typing import TYPE_CHECKING, Optional, List, Union
import asyncio
try:
import discord
except ImportError:
discord = None
if TYPE_CHECKING:
from royalnet.serf.discord import DiscordSerf
class PlayCommand(Command):
# TODO: possibly move this in another pack
name: str = "play"
description = "Download a file located at an URL and play it on Discord."
description = ""
syntax = "[url]"
async def run(self, args: CommandArgs, data: CommandData) -> None:
if self.interface.name != "discord":
raise UnsupportedError()
msg: "discord.Message" = data.message
guild: "discord.Guild" = msg.guild
url: str = args.joined()
response: dict = await self.interface.call_herald_event("discord", "play", {
"guild_id": guild.id,
"url": url,
})
message = f"▶️ Added to [c]{response['bard']['type']}[/c]:\n"
message += "\n".join([ytd['title'] for ytd in response['added']])
await data.reply(message)
url = args.joined()
response: dict = await self.interface.call_herald_event("discord", "play", url=url)
await data.reply("blah")

View file

@ -29,7 +29,7 @@ class SummonCommand(Command):
member = None
guild = None
name = args.joined()
response: dict = await self.interface.call_herald_event("discord", "summon", {
response: dict = await self.interface.call_herald_event("discord", "summon", **{
"channel_name": name,
"guild_id": guild.id if guild is not None else None,
"user_id": member.id if member is not None else None,

View file

@ -5,7 +5,7 @@ from .play import PlayEvent
# Enter the commands of your Pack here!
available_events = [
SummonEvent,
PlayEvent
PlayEvent,
]
# Don't change this, it should automatically generate __all__

View file

@ -1,12 +1,7 @@
from typing import Optional
from royalnet.commands import *
from royalnet.serf.discord import DiscordSerf
from royalnet.serf.discord.discordbard import YtdlDiscord, DiscordBard
from royalnet.utils import asyncify
import pickle
import logging
log = logging.getLogger(__name__)
from royalnet.serf.discord import DiscordSerf, PlayableYTDQueue
from royalnet.bard import YtdlDiscord
try:
import discord
@ -17,53 +12,12 @@ except ImportError:
class PlayEvent(Event):
name = "play"
async def run(self, *, url: str, guild_id: Optional[int] = None, guild_name: Optional[str] = None, **kwargs):
async def run(self, *, url: str):
if not isinstance(self.serf, DiscordSerf):
raise UnsupportedError("Play can't be called on interfaces other than Discord.")
raise UnsupportedError("Summon can't be called on interfaces other than Discord.")
if discord is None:
raise UnsupportedError("'discord' extra is not installed.")
# Variables
client = self.serf.client
# Find the guild
guild: Optional["discord.Guild"] = None
if guild_id is not None:
guild = client.get_guild(guild_id)
elif guild_name is not None:
for g in client.guilds:
if g.name == guild_name:
guild = g
break
if guild is None:
raise InvalidInputError("No guild_id or guild_name specified.")
log.debug(f"Selected guild: {guild}")
# Find the bard
bard: Optional[DiscordBard] = self.serf.bards.get(guild)
if bard is None:
raise CommandError("Bot is not connected to voice chat.")
# Create the YtdlDiscords
log.debug(f"Downloading: {url}")
try:
ytdl = await YtdlDiscord.from_url(url)
except Exception as exc:
breakpoint()
return
# Add the YtdlDiscords to the queue
log.debug(f"Adding to bard: {ytdl}")
for ytd in ytdl:
await bard.add(ytd)
# Run the bard
log.debug(f"Running voice for: {guild}")
# FIXME: sure?
self.loop.create_task(self.serf.voice_run(guild))
# Return the results
log.debug(f"Sending results...")
results = {
"added": [{
"title": ytd.info.title,
"embed_pickle": pickle.dumps(ytd.embed())
} for ytd in ytdl],
"bard": {
"type": bard.__class__.__name__,
}
}
return results
ytd = await YtdlDiscord.from_url(url)
self.serf.voice_players[0].playing.contents.append(ytd[0])
await self.serf.voice_players[0].start()
return {"ok": "ok"}

View file

@ -1,6 +1,6 @@
from typing import Optional
from royalnet.commands import *
from royalnet.serf.discord import DiscordSerf
from royalnet.serf.discord import DiscordSerf, VoicePlayer, PlayableYTDQueue
try:
import discord
@ -38,11 +38,13 @@ class SummonEvent(Event):
required_permissions=["connect", "speak"])
if channel is None:
raise InvalidInputError("No channels found with the specified name.")
# Create a new VoicePlayer
vp = VoicePlayer(loop=self.loop)
vp.playing = await PlayableYTDQueue.create()
# Connect to the channel
await self.serf.voice_connect(channel)
# Find the created bard
bard = self.serf.bards[channel.guild]
bard_peek = await bard.peek()
await vp.connect(channel)
# Add the created VoicePlayer to the list
self.serf.voice_players.append(vp)
# Reply to the request
return {
"channel": {
@ -51,9 +53,5 @@ class SummonEvent(Event):
"guild": {
"name": channel.guild.name,
},
},
"bard": {
"type": bard.__class__.__qualname__,
"peek": bard_peek,
}
}

View file

@ -4,10 +4,14 @@ It is pretty unstable, compared to the rest of the bot, but it *should* work."""
from .escape import escape
from .discordserf import DiscordSerf
from . import discordbard
from .playable import Playable
from .playableytdqueue import PlayableYTDQueue
from .voiceplayer import VoicePlayer
__all__ = [
"escape",
"DiscordSerf",
"discordbard",
"Playable",
"PlayableYTDQueue",
"VoicePlayer",
]

View file

@ -1,10 +1,12 @@
import asyncio
import logging
import warnings
from typing import Type, Optional, List, Union, Dict
from royalnet.commands import *
from royalnet.utils import asyncify
from royalnet.serf import Serf
from .escape import escape
from .voiceplayer import VoicePlayer
try:
@ -52,6 +54,9 @@ class DiscordSerf(Serf):
self.client = self.Client()
"""The custom :class:`discord.Client` instance."""
self.voice_players: List[VoicePlayer] = []
"""A :class:`list` of the :class:`VoicePlayer` in use by this :class:`DiscordSerf`."""
def interface_factory(self) -> Type[CommandInterface]:
# noinspection PyPep8Naming
GenericInterface = super().interface_factory()
@ -178,6 +183,7 @@ class DiscordSerf(Serf):
Returns:
Either a :class:`~discord.abc.GuildChannel`, or :const:`None` if no channels were found."""
warnings.warn("This function will be removed soon.", category=DeprecationWarning)
if accessible_to is None:
accessible_to = []
if required_permissions is None:
@ -225,23 +231,3 @@ class DiscordSerf(Serf):
return channels[0]
async def voice_run(self, guild: "discord.Guild"):
"""Send the data from the bard to the voice websocket for a specific client."""
bard: Optional[DiscordBard] = self.bards.get(guild)
if bard is None:
return
def finished_playing(error=None):
if error:
log.error(f"Finished playing with error: {error}")
return
self.loop.create_task(self.voice_run(guild))
if bard.now_playing is None:
fas = await bard.next()
if fas is None:
return
# FIXME: possible race condition here, pls check
bard = self.bards.get(guild)
if bard.voice_client is not None and bard.voice_client.is_connected():
bard.voice_client.play(fas, after=finished_playing)

View file

@ -36,3 +36,7 @@ class PlayerNotConnectedError(VoicePlayerError):
"""The :class:`VoicePlayer` isn't connected to the Discord voice servers.
Use :meth:`VoicePlayer.connect` first!"""
class PlayerAlreadyPlaying(VoicePlayerError):
"""The :class:`VoicePlayer` is already playing audio and cannot start playing audio again."""

View file

@ -1,3 +1,4 @@
import logging
from typing import Optional, AsyncGenerator, Tuple, Any, Dict
try:
import discord
@ -5,11 +6,30 @@ except ImportError:
discord = None
log = logging.getLogger(__name__)
class Playable:
"""An abstract class representing something that can be played back in a :class:`VoicePlayer`."""
def __init__(self):
self.generator: \
Optional[AsyncGenerator[Optional["discord.AudioSource"], Tuple[Tuple[Any, ...], Dict[str, Any]]]] = None
"""Create a :class:`Playable`.
Warning:
Avoid using this method, as it does not initialize the generator! Use :meth:`.create` instead."""
log.debug("Creating a Playable...")
self.generator: Optional[AsyncGenerator[Optional["discord.AudioSource"],
Tuple[Tuple[Any, ...], Dict[str, Any]]]] = self._generator()
# PyCharm doesn't like what I'm doing here.
# noinspection PyTypeChecker
@classmethod
async def create(cls, *args, **kwargs):
"""Create a :class:`Playable` and initialize its generator."""
playable = cls(*args, **kwargs)
log.debug("Sending None to the generator...")
await playable.generator.asend(None)
log.debug("Playable ready!")
return playable
async def next(self, *args, **kwargs) -> Optional["discord.AudioSource"]:
"""Get the next :class:`discord.AudioSource` that should be played.
@ -23,7 +43,10 @@ class Playable:
:const:`None` if there is nothing available to play, otherwise the :class:`discord.AudioSource` that should
be played.
"""
return await self.generator.asend((args, kwargs,))
log.debug("Getting next AudioSource...")
audio_source: Optional["discord.AudioSource"] = await self.generator.asend((args, kwargs,))
log.debug(f"Next: {audio_source}")
return audio_source
async def _generator(self) \
-> AsyncGenerator[Optional["discord.AudioSource"], Tuple[Tuple[Any, ...], Dict[str, Any]]]:

View file

@ -1,3 +1,4 @@
import logging
from typing import Optional, List, AsyncGenerator, Tuple, Any, Dict
from royalnet.bard import YtdlDiscord
from .playable import Playable
@ -7,6 +8,9 @@ except ImportError:
discord = None
log = logging.getLogger(__name__)
class PlayableYTDQueue(Playable):
"""A queue of :class:`YtdlDiscord` to be played in sequence."""
def __init__(self, start_with: Optional[List[YtdlDiscord]] = None):
@ -14,24 +18,28 @@ class PlayableYTDQueue(Playable):
self.contents: List[YtdlDiscord] = []
if start_with is not None:
self.contents = [*self.contents, *start_with]
log.debug(f"Created new PlayableYTDQueue containing: {self.contents}")
async def _generator(self) \
-> AsyncGenerator[Optional["discord.AudioSource"], Tuple[Tuple[Any, ...], Dict[str, Any]]]:
yield
while True:
log.debug(f"Dequeuing an item...")
try:
# Try to get the first YtdlDiscord of the queue
ytd: YtdlDiscord = self.contents.pop(0)
except IndexError:
# If there isn't anything, yield None
log.debug(f"Nothing to dequeue, yielding None.")
yield None
continue
try:
# Create a FileAudioSource from the YtdlDiscord
# If the file hasn't been fetched / downloaded / converted yet, it will do so before yielding
async with ytd.spawn_audiosource() as fas:
# Yield the resulting AudioSource
yield fas
finally:
# Delete the YtdlDiscord file
await ytd.delete_asap()
log.debug(f"Yielding FileAudioSource from: {ytd}")
# Create a FileAudioSource from the YtdlDiscord
# If the file hasn't been fetched / downloaded / converted yet, it will do so before yielding
async with ytd.spawn_audiosource() as fas:
# Yield the resulting AudioSource
yield fas
# Delete the YtdlDiscord file
log.debug(f"Deleting: {ytd}")
await ytd.delete_asap()
log.debug(f"Deleted successfully!")

View file

@ -1,4 +1,5 @@
import asyncio
import logging
from typing import Optional
from .errors import *
from .playable import Playable
@ -7,11 +8,17 @@ try:
except ImportError:
discord = None
log = logging.getLogger(__name__)
class VoicePlayer:
def __init__(self):
def __init__(self, *, loop: Optional[asyncio.AbstractEventLoop] = None):
self.voice_client: Optional["discord.VoiceClient"] = None
self.playing: Optional[Playable] = None
if loop is None:
self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
else:
self.loop = loop
async def connect(self, channel: "discord.VoiceChannel") -> "discord.VoiceClient":
"""Connect the :class:`VoicePlayer` to a :class:`discord.VoiceChannel`, creating a :class:`discord.VoiceClient`
@ -32,6 +39,7 @@ class VoicePlayer:
"""
if self.voice_client is not None and self.voice_client.is_connected():
raise PlayerAlreadyConnectedError()
log.debug(f"Connecting to: {channel}")
try:
self.voice_client = await channel.connect()
except asyncio.TimeoutError:
@ -51,6 +59,7 @@ class VoicePlayer:
"""
if self.voice_client is None or not self.voice_client.is_connected():
raise PlayerNotConnectedError()
log.debug(f"Disconnecting...")
await self.voice_client.disconnect(force=True)
self.voice_client = None
@ -58,15 +67,37 @@ class VoicePlayer:
"""Move the :class:`VoicePlayer` to a different channel.
This requires the :class:`VoicePlayer` to already be connected, and for the passed :class:`discord.VoiceChannel`
to be in the same :class:`discord.Guild` as """
to be in the same :class:`discord.Guild` of the :class:`VoicePlayer`."""
if self.voice_client is None or not self.voice_client.is_connected():
raise PlayerNotConnectedError()
if self.voice_client.guild != channel.guild:
raise ValueError("Can't move between two guilds.")
log.debug(f"Moving to: {channel}")
await self.voice_client.move_to(channel)
async def start(self):
"""Start playing music on the :class:`discord.VoiceClient`."""
"""Start playing music on the :class:`discord.VoiceClient`.
Info:
Doesn't pass any ``*args`` or ``**kwargs`` to the :class:`Playable`.
"""
if self.voice_client is None or not self.voice_client.is_connected():
raise PlayerNotConnectedError()
if self.voice_client.is_playing():
raise PlayerAlreadyPlaying()
log.debug("Getting next AudioSource...")
next_source: Optional["discord.AudioSource"] = await self.playing.next()
if next_source is None:
log.debug(f"Next source would be None, stopping here...")
return
log.debug(f"Next: {next_source}")
self.voice_client.play(next_source, after=self._playback_ended)
def _playback_ended(self, error=None):
...
def _playback_ended(self, error: Exception = None):
"""An helper method that is called when the :attr:`.voice_client._player` has finished playing."""
if error is not None:
# TODO: capture exception with Sentry
log.error(f"Error during playback: {error}")
return
# Create a new task to create
self.loop.create_task(self.start())