mirror of
https://github.com/RYGhub/royalnet.git
synced 2024-11-23 19:44:20 +00:00
IT WOOOOOOOOOOOOOOORKS
This commit is contained in:
parent
2b24d8cd09
commit
88a9ef35ca
14 changed files with 121 additions and 120 deletions
|
@ -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>
|
|
@ -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" />
|
||||
|
|
|
@ -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>
|
|
@ -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")
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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__
|
||||
|
|
|
@ -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"}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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]]]:
|
||||
|
|
|
@ -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:
|
||||
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
|
||||
finally:
|
||||
# Delete the YtdlDiscord file
|
||||
log.debug(f"Deleting: {ytd}")
|
||||
await ytd.delete_asap()
|
||||
log.debug(f"Deleted successfully!")
|
||||
|
|
|
@ -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())
|
||||
|
|
Loading…
Reference in a new issue