diff --git a/.gitignore b/.gitignore
index c90f79d9..dffa20b1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@
.idea/misc.xml
.idea/royalnet.iml
dist/
+**/__pycache__/
diff --git a/.idea/modules.xml b/.idea/modules.xml
index 502cd4e7..3b66fe55 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -3,6 +3,7 @@
+
\ No newline at end of file
diff --git a/.idea/royalnet.iml b/.idea/royalnet.iml
index 4832e066..cb78e430 100644
--- a/.idea/royalnet.iml
+++ b/.idea/royalnet.iml
@@ -3,11 +3,13 @@
+
+
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index 94a25f7f..924c324e 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -2,5 +2,7 @@
+
+
\ No newline at end of file
diff --git a/royalnet/bard/__init__.py b/royalnet/bard/__init__.py
index 7e1e3f56..5ead8635 100644
--- a/royalnet/bard/__init__.py
+++ b/royalnet/bard/__init__.py
@@ -1,7 +1,7 @@
from .ytdlinfo import YtdlInfo
from .ytdlfile import YtdlFile
from .ytdlmp3 import YtdlMp3
-from .errors import *
+from .ytdldiscord import YtdlDiscord
try:
from .fileaudiosource import FileAudioSource
@@ -13,10 +13,6 @@ __all__ = [
"YtdlInfo",
"YtdlFile",
"YtdlMp3",
- "BardError",
- "YtdlError",
- "NotFoundError",
- "MultipleFilesError",
+ "YtdlDiscord",
"FileAudioSource",
- "UnsupportedError",
]
diff --git a/royalnet/serf/discord/playable.py b/royalnet/serf/discord/playable.py
new file mode 100644
index 00000000..aeedf48a
--- /dev/null
+++ b/royalnet/serf/discord/playable.py
@@ -0,0 +1,38 @@
+from typing import Optional, AsyncGenerator, Tuple, Any, Dict
+try:
+ import discord
+except ImportError:
+ discord = None
+
+
+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
+
+ async def next(self, *args, **kwargs) -> Optional["discord.AudioSource"]:
+ """Get the next :class:`discord.AudioSource` that should be played.
+
+ Called when the :class:`Playable` is first attached to a :class:`VoicePlayer` and when a
+ :class:`discord.AudioSource` stops playing.
+
+ Args and kwargs can be used to pass data to the generator.
+
+ Returns:
+ :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,))
+
+ async def _generator(self) \
+ -> AsyncGenerator[Optional["discord.AudioSource"], Tuple[Tuple[Any, ...], Dict[str, Any]]]:
+ """Create an async generator that returns the next source to be played;
+ it can take a args+kwargs tuple in input to optionally select a different source.
+
+ Note:
+ For `weird Python reasons
+ `, the generator
+ should ``yield`` once before doing anything else."""
+ yield
+ raise NotImplementedError()
diff --git a/royalnet/serf/discord/playableytdqueue.py b/royalnet/serf/discord/playableytdqueue.py
new file mode 100644
index 00000000..1094244d
--- /dev/null
+++ b/royalnet/serf/discord/playableytdqueue.py
@@ -0,0 +1,37 @@
+from typing import Optional, List, AsyncGenerator, Tuple, Any, Dict
+from royalnet.bard import YtdlDiscord
+from .playable import Playable
+try:
+ import discord
+except ImportError:
+ discord = None
+
+
+class PlayableYTDQueue(Playable):
+ """A queue of :class:`YtdlDiscord` to be played in sequence."""
+ def __init__(self, start_with: Optional[List[YtdlDiscord]] = None):
+ super().__init__()
+ self.contents: List[YtdlDiscord] = []
+ if start_with is not None:
+ self.contents = [*self.contents, *start_with]
+
+ async def _generator(self) \
+ -> AsyncGenerator[Optional["discord.AudioSource"], Tuple[Tuple[Any, ...], Dict[str, Any]]]:
+ yield
+ while True:
+ 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
+ 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()
diff --git a/royalnet/serf/discord/voiceplayer.py b/royalnet/serf/discord/voiceplayer.py
index ed335bfc..5534122a 100644
--- a/royalnet/serf/discord/voiceplayer.py
+++ b/royalnet/serf/discord/voiceplayer.py
@@ -1,6 +1,7 @@
import asyncio
from typing import Optional
from .errors import *
+from .playable import Playable
try:
import discord
except ImportError:
@@ -10,7 +11,7 @@ except ImportError:
class VoicePlayer:
def __init__(self):
self.voice_client: Optional["discord.VoiceClient"] = None
- ...
+ self.playing: Optional[Playable] = None
async def connect(self, channel: "discord.VoiceChannel") -> "discord.VoiceClient":
"""Connect the :class:`VoicePlayer` to a :class:`discord.VoiceChannel`, creating a :class:`discord.VoiceClient`
@@ -29,7 +30,7 @@ class VoicePlayer:
GuildAlreadyConnectedError:
OpusNotLoadedError:
"""
- if self.voice_client is not None:
+ if self.voice_client is not None and self.voice_client.is_connected():
raise PlayerAlreadyConnectedError()
try:
self.voice_client = await channel.connect()
@@ -48,7 +49,7 @@ class VoicePlayer:
Raises:
PlayerNotConnectedError:
"""
- if self.voice_client is None:
+ if self.voice_client is None or not self.voice_client.is_connected():
raise PlayerNotConnectedError()
await self.voice_client.disconnect(force=True)
self.voice_client = None
@@ -58,5 +59,14 @@ class VoicePlayer:
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 """
+ if self.voice_client is None or not self.voice_client.is_connected():
+ raise PlayerNotConnectedError()
+ await self.voice_client.move_to(channel)
- ...
+ async def start(self):
+ """Start playing music on the :class:`discord.VoiceClient`."""
+ if self.voice_client is None or not self.voice_client.is_connected():
+ raise PlayerNotConnectedError()
+
+ def _playback_ended(self, error=None):
+ ...