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">
|
<component name="ProjectModuleManager">
|
||||||
<modules>
|
<modules>
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/royalnet.iml" filepath="$PROJECT_DIR$/.idea/royalnet.iml" />
|
<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>
|
</modules>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
|
@ -9,7 +9,6 @@
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="jdk" jdkName="Python 3.8 (royalnet-1MWM6-kd-py3.8)" jdkType="Python SDK" />
|
<orderEntry type="jdk" jdkName="Python 3.8 (royalnet-1MWM6-kd-py3.8)" jdkType="Python SDK" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
<orderEntry type="module" module-name="royalpack" />
|
|
||||||
</component>
|
</component>
|
||||||
<component name="TestRunnerService">
|
<component name="TestRunnerService">
|
||||||
<option name="PROJECT_TEST_RUNNER" value="Unittests" />
|
<option name="PROJECT_TEST_RUNNER" value="Unittests" />
|
||||||
|
|
|
@ -3,6 +3,5 @@
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
<mapping directory="$PROJECT_DIR$/docs_source/royalpack" vcs="Git" />
|
<mapping directory="$PROJECT_DIR$/docs_source/royalpack" vcs="Git" />
|
||||||
<mapping directory="$PROJECT_DIR$/../royalpack" vcs="Git" />
|
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
|
@ -1,30 +1,26 @@
|
||||||
from royalnet.commands import *
|
from royalnet.commands import *
|
||||||
|
from typing import TYPE_CHECKING, Optional, List, Union
|
||||||
|
import asyncio
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import discord
|
import discord
|
||||||
except ImportError:
|
except ImportError:
|
||||||
discord = None
|
discord = None
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from royalnet.serf.discord import DiscordSerf
|
||||||
|
|
||||||
|
|
||||||
class PlayCommand(Command):
|
class PlayCommand(Command):
|
||||||
# TODO: possibly move this in another pack
|
# TODO: possibly move this in another pack
|
||||||
|
|
||||||
name: str = "play"
|
name: str = "play"
|
||||||
|
|
||||||
description = "Download a file located at an URL and play it on Discord."
|
description = ""
|
||||||
|
|
||||||
syntax = "[url]"
|
syntax = "[url]"
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
||||||
if self.interface.name != "discord":
|
url = args.joined()
|
||||||
raise UnsupportedError()
|
response: dict = await self.interface.call_herald_event("discord", "play", url=url)
|
||||||
msg: "discord.Message" = data.message
|
await data.reply("blah")
|
||||||
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)
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ class SummonCommand(Command):
|
||||||
member = None
|
member = None
|
||||||
guild = None
|
guild = None
|
||||||
name = args.joined()
|
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,
|
"channel_name": name,
|
||||||
"guild_id": guild.id if guild is not None else None,
|
"guild_id": guild.id if guild is not None else None,
|
||||||
"user_id": member.id if member 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!
|
# Enter the commands of your Pack here!
|
||||||
available_events = [
|
available_events = [
|
||||||
SummonEvent,
|
SummonEvent,
|
||||||
PlayEvent
|
PlayEvent,
|
||||||
]
|
]
|
||||||
|
|
||||||
# Don't change this, it should automatically generate __all__
|
# Don't change this, it should automatically generate __all__
|
||||||
|
|
|
@ -1,12 +1,7 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from royalnet.commands import *
|
from royalnet.commands import *
|
||||||
from royalnet.serf.discord import DiscordSerf
|
from royalnet.serf.discord import DiscordSerf, PlayableYTDQueue
|
||||||
from royalnet.serf.discord.discordbard import YtdlDiscord, DiscordBard
|
from royalnet.bard import YtdlDiscord
|
||||||
from royalnet.utils import asyncify
|
|
||||||
import pickle
|
|
||||||
import logging
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import discord
|
import discord
|
||||||
|
@ -17,53 +12,12 @@ except ImportError:
|
||||||
class PlayEvent(Event):
|
class PlayEvent(Event):
|
||||||
name = "play"
|
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):
|
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:
|
if discord is None:
|
||||||
raise UnsupportedError("'discord' extra is not installed.")
|
raise UnsupportedError("'discord' extra is not installed.")
|
||||||
# Variables
|
ytd = await YtdlDiscord.from_url(url)
|
||||||
client = self.serf.client
|
self.serf.voice_players[0].playing.contents.append(ytd[0])
|
||||||
# Find the guild
|
await self.serf.voice_players[0].start()
|
||||||
guild: Optional["discord.Guild"] = None
|
return {"ok": "ok"}
|
||||||
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
|
|
|
@ -1,6 +1,6 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from royalnet.commands import *
|
from royalnet.commands import *
|
||||||
from royalnet.serf.discord import DiscordSerf
|
from royalnet.serf.discord import DiscordSerf, VoicePlayer, PlayableYTDQueue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import discord
|
import discord
|
||||||
|
@ -38,11 +38,13 @@ class SummonEvent(Event):
|
||||||
required_permissions=["connect", "speak"])
|
required_permissions=["connect", "speak"])
|
||||||
if channel is None:
|
if channel is None:
|
||||||
raise InvalidInputError("No channels found with the specified name.")
|
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
|
# Connect to the channel
|
||||||
await self.serf.voice_connect(channel)
|
await vp.connect(channel)
|
||||||
# Find the created bard
|
# Add the created VoicePlayer to the list
|
||||||
bard = self.serf.bards[channel.guild]
|
self.serf.voice_players.append(vp)
|
||||||
bard_peek = await bard.peek()
|
|
||||||
# Reply to the request
|
# Reply to the request
|
||||||
return {
|
return {
|
||||||
"channel": {
|
"channel": {
|
||||||
|
@ -51,9 +53,5 @@ class SummonEvent(Event):
|
||||||
"guild": {
|
"guild": {
|
||||||
"name": channel.guild.name,
|
"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 .escape import escape
|
||||||
from .discordserf import DiscordSerf
|
from .discordserf import DiscordSerf
|
||||||
from . import discordbard
|
from .playable import Playable
|
||||||
|
from .playableytdqueue import PlayableYTDQueue
|
||||||
|
from .voiceplayer import VoicePlayer
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"escape",
|
"escape",
|
||||||
"DiscordSerf",
|
"DiscordSerf",
|
||||||
"discordbard",
|
"Playable",
|
||||||
|
"PlayableYTDQueue",
|
||||||
|
"VoicePlayer",
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import warnings
|
||||||
from typing import Type, Optional, List, Union, Dict
|
from typing import Type, Optional, List, Union, Dict
|
||||||
from royalnet.commands import *
|
from royalnet.commands import *
|
||||||
from royalnet.utils import asyncify
|
from royalnet.utils import asyncify
|
||||||
from royalnet.serf import Serf
|
from royalnet.serf import Serf
|
||||||
from .escape import escape
|
from .escape import escape
|
||||||
|
from .voiceplayer import VoicePlayer
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -52,6 +54,9 @@ class DiscordSerf(Serf):
|
||||||
self.client = self.Client()
|
self.client = self.Client()
|
||||||
"""The custom :class:`discord.Client` instance."""
|
"""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]:
|
def interface_factory(self) -> Type[CommandInterface]:
|
||||||
# noinspection PyPep8Naming
|
# noinspection PyPep8Naming
|
||||||
GenericInterface = super().interface_factory()
|
GenericInterface = super().interface_factory()
|
||||||
|
@ -178,6 +183,7 @@ class DiscordSerf(Serf):
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Either a :class:`~discord.abc.GuildChannel`, or :const:`None` if no channels were found."""
|
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:
|
if accessible_to is None:
|
||||||
accessible_to = []
|
accessible_to = []
|
||||||
if required_permissions is None:
|
if required_permissions is None:
|
||||||
|
@ -225,23 +231,3 @@ class DiscordSerf(Serf):
|
||||||
|
|
||||||
return channels[0]
|
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.
|
"""The :class:`VoicePlayer` isn't connected to the Discord voice servers.
|
||||||
|
|
||||||
Use :meth:`VoicePlayer.connect` first!"""
|
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
|
from typing import Optional, AsyncGenerator, Tuple, Any, Dict
|
||||||
try:
|
try:
|
||||||
import discord
|
import discord
|
||||||
|
@ -5,11 +6,30 @@ except ImportError:
|
||||||
discord = None
|
discord = None
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Playable:
|
class Playable:
|
||||||
"""An abstract class representing something that can be played back in a :class:`VoicePlayer`."""
|
"""An abstract class representing something that can be played back in a :class:`VoicePlayer`."""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.generator: \
|
"""Create a :class:`Playable`.
|
||||||
Optional[AsyncGenerator[Optional["discord.AudioSource"], Tuple[Tuple[Any, ...], Dict[str, Any]]]] = None
|
|
||||||
|
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"]:
|
async def next(self, *args, **kwargs) -> Optional["discord.AudioSource"]:
|
||||||
"""Get the next :class:`discord.AudioSource` that should be played.
|
"""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
|
:const:`None` if there is nothing available to play, otherwise the :class:`discord.AudioSource` that should
|
||||||
be played.
|
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) \
|
async def _generator(self) \
|
||||||
-> AsyncGenerator[Optional["discord.AudioSource"], Tuple[Tuple[Any, ...], Dict[str, Any]]]:
|
-> 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 typing import Optional, List, AsyncGenerator, Tuple, Any, Dict
|
||||||
from royalnet.bard import YtdlDiscord
|
from royalnet.bard import YtdlDiscord
|
||||||
from .playable import Playable
|
from .playable import Playable
|
||||||
|
@ -7,6 +8,9 @@ except ImportError:
|
||||||
discord = None
|
discord = None
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class PlayableYTDQueue(Playable):
|
class PlayableYTDQueue(Playable):
|
||||||
"""A queue of :class:`YtdlDiscord` to be played in sequence."""
|
"""A queue of :class:`YtdlDiscord` to be played in sequence."""
|
||||||
def __init__(self, start_with: Optional[List[YtdlDiscord]] = None):
|
def __init__(self, start_with: Optional[List[YtdlDiscord]] = None):
|
||||||
|
@ -14,24 +18,28 @@ class PlayableYTDQueue(Playable):
|
||||||
self.contents: List[YtdlDiscord] = []
|
self.contents: List[YtdlDiscord] = []
|
||||||
if start_with is not None:
|
if start_with is not None:
|
||||||
self.contents = [*self.contents, *start_with]
|
self.contents = [*self.contents, *start_with]
|
||||||
|
log.debug(f"Created new PlayableYTDQueue containing: {self.contents}")
|
||||||
|
|
||||||
async def _generator(self) \
|
async def _generator(self) \
|
||||||
-> AsyncGenerator[Optional["discord.AudioSource"], Tuple[Tuple[Any, ...], Dict[str, Any]]]:
|
-> AsyncGenerator[Optional["discord.AudioSource"], Tuple[Tuple[Any, ...], Dict[str, Any]]]:
|
||||||
yield
|
yield
|
||||||
while True:
|
while True:
|
||||||
|
log.debug(f"Dequeuing an item...")
|
||||||
try:
|
try:
|
||||||
# Try to get the first YtdlDiscord of the queue
|
# Try to get the first YtdlDiscord of the queue
|
||||||
ytd: YtdlDiscord = self.contents.pop(0)
|
ytd: YtdlDiscord = self.contents.pop(0)
|
||||||
except IndexError:
|
except IndexError:
|
||||||
# If there isn't anything, yield None
|
# If there isn't anything, yield None
|
||||||
|
log.debug(f"Nothing to dequeue, yielding None.")
|
||||||
yield None
|
yield None
|
||||||
continue
|
continue
|
||||||
try:
|
log.debug(f"Yielding FileAudioSource from: {ytd}")
|
||||||
# Create a FileAudioSource from the YtdlDiscord
|
# Create a FileAudioSource from the YtdlDiscord
|
||||||
# If the file hasn't been fetched / downloaded / converted yet, it will do so before yielding
|
# If the file hasn't been fetched / downloaded / converted yet, it will do so before yielding
|
||||||
async with ytd.spawn_audiosource() as fas:
|
async with ytd.spawn_audiosource() as fas:
|
||||||
# Yield the resulting AudioSource
|
# Yield the resulting AudioSource
|
||||||
yield fas
|
yield fas
|
||||||
finally:
|
# Delete the YtdlDiscord file
|
||||||
# Delete the YtdlDiscord file
|
log.debug(f"Deleting: {ytd}")
|
||||||
await ytd.delete_asap()
|
await ytd.delete_asap()
|
||||||
|
log.debug(f"Deleted successfully!")
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from .errors import *
|
from .errors import *
|
||||||
from .playable import Playable
|
from .playable import Playable
|
||||||
|
@ -7,11 +8,17 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
discord = None
|
discord = None
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class VoicePlayer:
|
class VoicePlayer:
|
||||||
def __init__(self):
|
def __init__(self, *, loop: Optional[asyncio.AbstractEventLoop] = None):
|
||||||
self.voice_client: Optional["discord.VoiceClient"] = None
|
self.voice_client: Optional["discord.VoiceClient"] = None
|
||||||
self.playing: Optional[Playable] = 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":
|
async def connect(self, channel: "discord.VoiceChannel") -> "discord.VoiceClient":
|
||||||
"""Connect the :class:`VoicePlayer` to a :class:`discord.VoiceChannel`, creating a :class:`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():
|
if self.voice_client is not None and self.voice_client.is_connected():
|
||||||
raise PlayerAlreadyConnectedError()
|
raise PlayerAlreadyConnectedError()
|
||||||
|
log.debug(f"Connecting to: {channel}")
|
||||||
try:
|
try:
|
||||||
self.voice_client = await channel.connect()
|
self.voice_client = await channel.connect()
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
|
@ -51,6 +59,7 @@ class VoicePlayer:
|
||||||
"""
|
"""
|
||||||
if self.voice_client is None or not self.voice_client.is_connected():
|
if self.voice_client is None or not self.voice_client.is_connected():
|
||||||
raise PlayerNotConnectedError()
|
raise PlayerNotConnectedError()
|
||||||
|
log.debug(f"Disconnecting...")
|
||||||
await self.voice_client.disconnect(force=True)
|
await self.voice_client.disconnect(force=True)
|
||||||
self.voice_client = None
|
self.voice_client = None
|
||||||
|
|
||||||
|
@ -58,15 +67,37 @@ class VoicePlayer:
|
||||||
"""Move the :class:`VoicePlayer` to a different channel.
|
"""Move the :class:`VoicePlayer` to a different channel.
|
||||||
|
|
||||||
This requires the :class:`VoicePlayer` to already be connected, and for the passed :class:`discord.VoiceChannel`
|
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():
|
if self.voice_client is None or not self.voice_client.is_connected():
|
||||||
raise PlayerNotConnectedError()
|
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)
|
await self.voice_client.move_to(channel)
|
||||||
|
|
||||||
async def start(self):
|
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():
|
if self.voice_client is None or not self.voice_client.is_connected():
|
||||||
raise PlayerNotConnectedError()
|
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