mirror of
https://github.com/RYGhub/royalnet.git
synced 2024-11-27 13:34:28 +00:00
rework is finally finished
This commit is contained in:
parent
d570e09e90
commit
a9967918ff
9 changed files with 72 additions and 40 deletions
|
@ -1,8 +1,9 @@
|
||||||
"""Video and audio downloading related classes, mainly used for Discord voice bots."""
|
"""Video and audio downloading related classes, mainly used for Discord voice bots."""
|
||||||
|
|
||||||
from .playmodes import PlayMode, Playlist, Pool
|
from . import playmodes
|
||||||
from .ytdlfile import YtdlFile, YtdlInfo
|
from .ytdlinfo import YtdlInfo
|
||||||
from .royalpcmfile import RoyalPCMFile
|
from .ytdlfile import YtdlFile
|
||||||
from .royalpcmaudio import RoyalPCMAudio
|
from .fileaudiosource import FileAudioSource
|
||||||
|
from .ytdldiscord import YtdlDiscord
|
||||||
|
|
||||||
__all__ = ["PlayMode", "Playlist", "Pool", "YtdlFile", "YtdlInfo", "RoyalPCMFile", "RoyalPCMAudio"]
|
__all__ = ["playmodes", "YtdlInfo", "YtdlFile", "FileAudioSource", "YtdlDiscord"]
|
||||||
|
|
|
@ -7,7 +7,7 @@ class FileAudioSource(discord.AudioSource):
|
||||||
The stream should be in the usual PCM encoding.
|
The stream should be in the usual PCM encoding.
|
||||||
|
|
||||||
Warning:
|
Warning:
|
||||||
This AudioSource will consume (and close) the passed stream"""
|
This AudioSource will consume (and close) the passed stream."""
|
||||||
|
|
||||||
def __init__(self, file):
|
def __init__(self, file):
|
||||||
self.file = file
|
self.file = file
|
||||||
|
@ -29,10 +29,10 @@ class FileAudioSource(discord.AudioSource):
|
||||||
"""Reads 20ms worth of audio.
|
"""Reads 20ms worth of audio.
|
||||||
|
|
||||||
If the audio is complete, then returning an empty :py:class:`bytes`-like object to signal this is the way to do so."""
|
If the audio is complete, then returning an empty :py:class:`bytes`-like object to signal this is the way to do so."""
|
||||||
data: bytes = self.file.read(discord.opus.Encoder.FRAME_SIZE)
|
|
||||||
# If the stream is closed, it should stop playing immediatly
|
# If the stream is closed, it should stop playing immediatly
|
||||||
if self.file.closed:
|
if self.file.closed:
|
||||||
return b""
|
return b""
|
||||||
|
data: bytes = self.file.read(discord.opus.Encoder.FRAME_SIZE)
|
||||||
# If there is no more data to be streamed
|
# If there is no more data to be streamed
|
||||||
if len(data) != discord.opus.Encoder.FRAME_SIZE:
|
if len(data) != discord.opus.Encoder.FRAME_SIZE:
|
||||||
# Close the file
|
# Close the file
|
||||||
|
|
|
@ -83,9 +83,10 @@ class Playlist(PlayMode):
|
||||||
next_video = self.list.pop(0)
|
next_video = self.list.pop(0)
|
||||||
except IndexError:
|
except IndexError:
|
||||||
self.now_playing = None
|
self.now_playing = None
|
||||||
|
yield None
|
||||||
else:
|
else:
|
||||||
self.now_playing = next_video
|
self.now_playing = next_video
|
||||||
yield self.now_playing
|
yield next_video.spawn_audiosource()
|
||||||
if self.now_playing is not None:
|
if self.now_playing is not None:
|
||||||
self.now_playing.delete()
|
self.now_playing.delete()
|
||||||
|
|
||||||
|
@ -130,7 +131,7 @@ class Pool(PlayMode):
|
||||||
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
|
self.now_playing = next_video
|
||||||
yield next_video
|
yield next_video.spawn_audiosource()
|
||||||
|
|
||||||
def add(self, item) -> None:
|
def add(self, item) -> None:
|
||||||
self.pool.append(item)
|
self.pool.append(item)
|
||||||
|
|
|
@ -3,6 +3,7 @@ import discord
|
||||||
import re
|
import re
|
||||||
import ffmpeg
|
import ffmpeg
|
||||||
import os
|
import os
|
||||||
|
from .ytdlinfo import YtdlInfo
|
||||||
from .ytdlfile import YtdlFile
|
from .ytdlfile import YtdlFile
|
||||||
from .fileaudiosource import FileAudioSource
|
from .fileaudiosource import FileAudioSource
|
||||||
|
|
||||||
|
@ -11,6 +12,7 @@ class YtdlDiscord:
|
||||||
def __init__(self, ytdl_file: YtdlFile):
|
def __init__(self, ytdl_file: YtdlFile):
|
||||||
self.ytdl_file: YtdlFile = ytdl_file
|
self.ytdl_file: YtdlFile = ytdl_file
|
||||||
self.pcm_filename: typing.Optional[str] = None
|
self.pcm_filename: typing.Optional[str] = None
|
||||||
|
self._fas_spawned: typing.List[FileAudioSource] = []
|
||||||
|
|
||||||
def pcm_available(self):
|
def pcm_available(self):
|
||||||
return self.pcm_filename is not None and os.path.exists(self.pcm_filename)
|
return self.pcm_filename is not None and os.path.exists(self.pcm_filename)
|
||||||
|
@ -35,14 +37,40 @@ class YtdlDiscord:
|
||||||
if not self.pcm_available():
|
if not self.pcm_available():
|
||||||
self.convert_to_pcm()
|
self.convert_to_pcm()
|
||||||
|
|
||||||
def to_audiosource(self) -> discord.AudioSource:
|
def spawn_audiosource(self) -> discord.AudioSource:
|
||||||
if not self.pcm_available():
|
if not self.pcm_available():
|
||||||
raise FileNotFoundError("File hasn't been converted to PCM yet")
|
raise FileNotFoundError("File hasn't been converted to PCM yet")
|
||||||
stream = open(self.pcm_filename, "rb")
|
stream = open(self.pcm_filename, "rb")
|
||||||
return FileAudioSource(stream)
|
source = FileAudioSource(stream)
|
||||||
|
# FIXME: it's a intentional memory leak
|
||||||
|
self._fas_spawned.append(source)
|
||||||
|
return source
|
||||||
|
|
||||||
def delete(self) -> None:
|
def delete(self) -> None:
|
||||||
if self.pcm_available():
|
if self.pcm_available():
|
||||||
|
for source in self._fas_spawned:
|
||||||
|
if not source.file.closed:
|
||||||
|
source.file.close()
|
||||||
os.remove(self.pcm_filename)
|
os.remove(self.pcm_filename)
|
||||||
self.pcm_filename = None
|
self.pcm_filename = None
|
||||||
self.ytdl_file.delete()
|
self.ytdl_file.delete()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_from_url(cls, url, **ytdl_args) -> typing.List["YtdlDiscord"]:
|
||||||
|
files = YtdlFile.download_from_url(url, **ytdl_args)
|
||||||
|
dfiles = []
|
||||||
|
for file in files:
|
||||||
|
dfile = YtdlDiscord(file)
|
||||||
|
dfiles.append(dfile)
|
||||||
|
return dfiles
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_and_ready_from_url(cls, url, **ytdl_args) -> typing.List["YtdlDiscord"]:
|
||||||
|
dfiles = cls.create_from_url(url, **ytdl_args)
|
||||||
|
for dfile in dfiles:
|
||||||
|
dfile.ready_up()
|
||||||
|
return dfiles
|
||||||
|
|
||||||
|
@property
|
||||||
|
def info(self) -> typing.Optional[YtdlInfo]:
|
||||||
|
return self.ytdl_file.info
|
||||||
|
|
|
@ -8,7 +8,7 @@ from ..utils import asyncify, Call, Command, discord_escape
|
||||||
from ..error import UnregisteredError, NoneFoundError, TooManyFoundError, InvalidConfigError, RoyalnetResponseError
|
from ..error import UnregisteredError, NoneFoundError, TooManyFoundError, InvalidConfigError, RoyalnetResponseError
|
||||||
from ..network import RoyalnetConfig, Request, ResponseSuccess, ResponseError
|
from ..network import RoyalnetConfig, Request, ResponseSuccess, ResponseError
|
||||||
from ..database import DatabaseConfig
|
from ..database import DatabaseConfig
|
||||||
from ..audio import PlayMode, Playlist, RoyalPCMAudio
|
from ..audio import playmodes, YtdlDiscord
|
||||||
|
|
||||||
log = _logging.getLogger(__name__)
|
log = _logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ class DiscordBot(GenericBot):
|
||||||
def _init_voice(self):
|
def _init_voice(self):
|
||||||
"""Initialize the variables needed for the connection to voice chat."""
|
"""Initialize the variables needed for the connection to voice chat."""
|
||||||
log.debug(f"Creating music_data dict")
|
log.debug(f"Creating music_data dict")
|
||||||
self.music_data: typing.Dict[discord.Guild, PlayMode] = {}
|
self.music_data: typing.Dict[discord.Guild, playmodes.PlayMode] = {}
|
||||||
|
|
||||||
def _call_factory(self) -> typing.Type[Call]:
|
def _call_factory(self) -> typing.Type[Call]:
|
||||||
log.debug(f"Creating DiscordCall")
|
log.debug(f"Creating DiscordCall")
|
||||||
|
@ -100,7 +100,7 @@ class DiscordBot(GenericBot):
|
||||||
# Create a music_data entry, if it doesn't exist; default is a Playlist
|
# Create a music_data entry, if it doesn't exist; default is a Playlist
|
||||||
if not self.music_data.get(channel.guild):
|
if not self.music_data.get(channel.guild):
|
||||||
log.debug(f"Creating music_data for {channel.guild}")
|
log.debug(f"Creating music_data for {channel.guild}")
|
||||||
self.music_data[channel.guild] = Playlist()
|
self.music_data[channel.guild] = playmodes.Playlist()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def on_message(message: discord.Message):
|
async def on_message(message: discord.Message):
|
||||||
|
@ -216,12 +216,12 @@ class DiscordBot(GenericBot):
|
||||||
await self.client.connect()
|
await self.client.connect()
|
||||||
# TODO: how to stop?
|
# TODO: how to stop?
|
||||||
|
|
||||||
async def add_to_music_data(self, audio_sources: typing.List[RoyalPCMAudio], guild: discord.Guild):
|
async def add_to_music_data(self, dfiles: typing.List[YtdlDiscord], guild: discord.Guild):
|
||||||
"""Add a file to the corresponding music_data object."""
|
"""Add a list of :py:class:`royalnet.audio.YtdlDiscord` to the corresponding music_data object."""
|
||||||
guild_music_data = self.music_data[guild]
|
guild_music_data = self.music_data[guild]
|
||||||
for audio_source in audio_sources:
|
for dfile in dfiles:
|
||||||
log.debug(f"Adding {audio_source} to music_data")
|
log.debug(f"Adding {dfile} to music_data")
|
||||||
guild_music_data.add(audio_source)
|
guild_music_data.add(dfile)
|
||||||
if guild_music_data.now_playing is None:
|
if guild_music_data.now_playing is None:
|
||||||
await self.advance_music_data(guild)
|
await self.advance_music_data(guild)
|
||||||
|
|
||||||
|
@ -229,7 +229,7 @@ class DiscordBot(GenericBot):
|
||||||
"""Try to play the next song, while it exists. Otherwise, just return."""
|
"""Try to play the next song, while it exists. Otherwise, just return."""
|
||||||
guild_music_data = self.music_data[guild]
|
guild_music_data = self.music_data[guild]
|
||||||
voice_client = self.client.find_voice_client_by_guild(guild)
|
voice_client = self.client.find_voice_client_by_guild(guild)
|
||||||
next_source: RoyalPCMAudio = await guild_music_data.next()
|
next_source: discord.AudioSource = await guild_music_data.next()
|
||||||
await self.update_activity_with_source_title()
|
await self.update_activity_with_source_title()
|
||||||
if next_source is None:
|
if next_source is None:
|
||||||
log.debug(f"Ending playback chain")
|
log.debug(f"Ending playback chain")
|
||||||
|
@ -252,16 +252,14 @@ class DiscordBot(GenericBot):
|
||||||
log.debug(f"Updating current Activity: setting to None, as multiple guilds are using the bot")
|
log.debug(f"Updating current Activity: setting to None, as multiple guilds are using the bot")
|
||||||
await self.client.change_presence(status=discord.Status.online)
|
await self.client.change_presence(status=discord.Status.online)
|
||||||
return
|
return
|
||||||
# FIXME: PyCharm faulty inspection?
|
play_mode: playmodes.PlayMode = self.music_data[list(self.music_data)[0]]
|
||||||
# noinspection PyUnresolvedReferences
|
|
||||||
play_mode: PlayMode = list(self.music_data.items())[0][1]
|
|
||||||
now_playing = play_mode.now_playing
|
now_playing = play_mode.now_playing
|
||||||
if now_playing is None:
|
if now_playing is None:
|
||||||
# No songs are playing now
|
# No songs are playing now
|
||||||
log.debug(f"Updating current Activity: setting to None, as nothing is currently being played")
|
log.debug(f"Updating current Activity: setting to None, as nothing is currently being played")
|
||||||
await self.client.change_presence(status=discord.Status.online)
|
await self.client.change_presence(status=discord.Status.online)
|
||||||
return
|
return
|
||||||
log.debug(f"Updating current Activity: listening to {now_playing.rpf.info.title}")
|
log.debug(f"Updating current Activity: listening to {now_playing.info.title}")
|
||||||
await self.client.change_presence(activity=discord.Activity(name=now_playing.rpf.info.title,
|
await self.client.change_presence(activity=discord.Activity(name=now_playing.info.title,
|
||||||
type=discord.ActivityType.listening),
|
type=discord.ActivityType.listening),
|
||||||
status=discord.Status.online)
|
status=discord.Status.online)
|
||||||
|
|
|
@ -6,11 +6,17 @@ import pickle
|
||||||
from ..utils import Command, Call, NetworkHandler, asyncify
|
from ..utils import Command, Call, NetworkHandler, asyncify
|
||||||
from ..network import Request, ResponseSuccess
|
from ..network import Request, ResponseSuccess
|
||||||
from ..error import TooManyFoundError, NoneFoundError
|
from ..error import TooManyFoundError, NoneFoundError
|
||||||
from ..audio import RoyalPCMAudio
|
from ..audio import YtdlDiscord
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from ..bots import DiscordBot
|
from ..bots import DiscordBot
|
||||||
|
|
||||||
|
|
||||||
|
ytdl_args = {
|
||||||
|
"format": "bestaudio",
|
||||||
|
"outtmpl": f"./downloads/%(autonumber)s_%(title)s.%(ext)s"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class PlayNH(NetworkHandler):
|
class PlayNH(NetworkHandler):
|
||||||
message_type = "music_play"
|
message_type = "music_play"
|
||||||
|
|
||||||
|
@ -32,16 +38,16 @@ class PlayNH(NetworkHandler):
|
||||||
raise Exception("No music_data for this guild")
|
raise Exception("No music_data for this guild")
|
||||||
# Start downloading
|
# Start downloading
|
||||||
if data["url"].startswith("http://") or data["url"].startswith("https://"):
|
if data["url"].startswith("http://") or data["url"].startswith("https://"):
|
||||||
audio_sources: typing.List[RoyalPCMAudio] = await asyncify(RoyalPCMAudio.create_from_url, data["url"])
|
dfiles: typing.List[YtdlDiscord] = await asyncify(YtdlDiscord.create_and_ready_from_url, data["url"], **ytdl_args)
|
||||||
else:
|
else:
|
||||||
audio_sources = await asyncify(RoyalPCMAudio.create_from_ytsearch, data["url"])
|
dfiles = await asyncify(YtdlDiscord.create_and_ready_from_url, f"ytsearch:{data['url']}", **ytdl_args)
|
||||||
await bot.add_to_music_data(audio_sources, guild)
|
await bot.add_to_music_data(dfiles, guild)
|
||||||
# Create response dictionary
|
# Create response dictionary
|
||||||
response = {
|
response = {
|
||||||
"videos": [{
|
"videos": [{
|
||||||
"title": source.rpf.info.title,
|
"title": dfile.info.title,
|
||||||
"discord_embed_pickle": str(pickle.dumps(source.rpf.info.to_discord_embed()))
|
"discord_embed_pickle": str(pickle.dumps(dfile.info.to_discord_embed()))
|
||||||
} for source in audio_sources]
|
} for dfile in dfiles]
|
||||||
}
|
}
|
||||||
return ResponseSuccess(response)
|
return ResponseSuccess(response)
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import typing
|
import typing
|
||||||
import asyncio
|
|
||||||
from ..utils import Command, Call, NetworkHandler
|
from ..utils import Command, Call, NetworkHandler
|
||||||
from ..network import Request, ResponseSuccess
|
from ..network import Request, ResponseSuccess
|
||||||
from ..error import NoneFoundError, TooManyFoundError, CurrentlyDisabledError
|
from ..error import NoneFoundError, TooManyFoundError
|
||||||
from ..audio import Playlist, Pool
|
from ..audio.playmodes import Playlist, Pool
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from ..bots import DiscordBot
|
from ..bots import DiscordBot
|
||||||
|
|
||||||
|
@ -30,8 +29,7 @@ class PlaymodeNH(NetworkHandler):
|
||||||
if data["mode_name"] == "playlist":
|
if data["mode_name"] == "playlist":
|
||||||
bot.music_data[guild] = Playlist()
|
bot.music_data[guild] = Playlist()
|
||||||
elif data["mode_name"] == "pool":
|
elif data["mode_name"] == "pool":
|
||||||
# bot.music_data[guild] = Pool()
|
bot.music_data[guild] = Pool()
|
||||||
raise CurrentlyDisabledError("Bug: https://github.com/royal-games/royalnet/issues/61")
|
|
||||||
else:
|
else:
|
||||||
raise ValueError("No such PlayMode")
|
raise ValueError("No such PlayMode")
|
||||||
return ResponseSuccess()
|
return ResponseSuccess()
|
||||||
|
|
|
@ -37,8 +37,8 @@ class QueueNH(NetworkHandler):
|
||||||
"type": playmode.__class__.__name__,
|
"type": playmode.__class__.__name__,
|
||||||
"queue":
|
"queue":
|
||||||
{
|
{
|
||||||
"strings": [str(element.rpf.info) for element in queue],
|
"strings": [str(dfile.info) for dfile in queue],
|
||||||
"pickled_embeds": str(pickle.dumps([element.rpf.info.to_discord_embed() for element in queue]))
|
"pickled_embeds": str(pickle.dumps([dfile.info.to_discord_embed() for dfile in queue]))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ class VideoinfoCommand(Command):
|
||||||
@classmethod
|
@classmethod
|
||||||
async def common(cls, call: Call):
|
async def common(cls, call: Call):
|
||||||
url = call.args[0]
|
url = call.args[0]
|
||||||
info_list = await asyncify(YtdlInfo.create_from_url, url)
|
info_list = await asyncify(YtdlInfo.retrieve_for_url, url)
|
||||||
for info in info_list:
|
for info in info_list:
|
||||||
info_dict = info.__dict__
|
info_dict = info.__dict__
|
||||||
message = f"🔍 Dati di [b]{info}[/b]:\n"
|
message = f"🔍 Dati di [b]{info}[/b]:\n"
|
||||||
|
|
Loading…
Reference in a new issue